Constants and pure functions in Python: how to do it right | Andros Fenollosa
Under the functional programming paradigm, functions should be pure: the same input always produces the same output, with no side effects. But sometimes theory clashes with reality. When you use constants you start to depend on scope. There are variables external to the function. Constants, by definition, don't break purity since they are... constant. The result is predictable. However, we make testing harder, since we have to import them or create mocks, the code starts to be less readable, becomes less portable, and we also add an element of fragility since we can't control their value or the location of those external values.
An example of the above would be:
TAX = 0.21
def calculate_total(price: float) -> float:<br>return price + (price * TAX)<br>What happens if the tax changes between countries? And what if wherever I call calculate_total already has its own TAX?
Let me give you some solutions.
Dependency injection with functools.partial
You can use partial application with functools.partial. The base functions receive everything as parameters and then we "fix" the constants to create the final versions.
from functools import partial
def calculate_total(tax: float, price: float) -> float:<br>return price + (price * tax)
def apply_discount(discount: float, price: float) -> float:<br>return price - discount
TAX = 0.21<br>BASE_DISCOUNT = 5.0
calculate_total_spain = partial(calculate_total, TAX)<br>apply_fixed_discount = partial(apply_discount, BASE_DISCOUNT)
print(calculate_total_spain(100)) # 121.0<br>Here we gain maximum purity. The base functions are one hundred percent reusable, easy to test (you just pass different configurations as arguments) and isolated from the environment.
The cost is some visual complexity if your team isn't used to partial.
Closures
Another way is to encapsulate the functions inside a constructor function, a kind of factory.
def create_pricing_system(tax: float, discount: float):
def calculate_total(price: float) -> float:<br>return price + (price * tax)
def apply_discount(price: float) -> float:<br>return price - discount
def process_invoice(price: float) -> float:<br>return apply_discount(calculate_total(price))
return calculate_total, apply_discount, process_invoice
calculate, discount_fn, process = create_pricing_system(0.21, 5.0)<br>The constants are trapped in the closure, and we logically group the functions that share context without resorting to classes, thus avoiding mutable state.
The downside is that it can make reading harder if the inner functions grow too large.
Conclusions
Python is multiparadigm, so you're not forced to marry a single approach. The important thing is to be aware of the trade-off: the more a function looks outward, the easier it is to write, but the harder it is to test in isolation.
Dependency injection with functools.partial
Closures
Conclusions
This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.
Will you buy me a coffee?
Support me on Ko-fi
Comments
page#run"<br>data-liveview-function="show_comment_email"<br>data-id="5afd804f"<br>data-type="article"<br>>Leave a comment
There are no comments yet.
You may also like
page#run"<br>data-liveview-function="navigate_article"<br>data-uuid="dd5a0746">
web
python
django
django liveview
My website is now ~2.8x faster after converting it to a Django LiveView SPA
page#run"<br>data-liveview-function="navigate_article"<br>data-uuid="5d4edfbf">
docker
python
Quick Docker Tutorial to Run a Python Script
page#run"<br>data-liveview-function="navigate_article"<br>data-uuid="d6eb1348">
python
testing
Learn how to structure a test
Visitors in real time
You are alone: 🐱