Constants and pure functions in Python: how to do it right

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:
    return price + (price * TAX)

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:
    return price + (price * tax)


def apply_discount(discount: float, price: float) -> float:
    return price - discount


TAX = 0.21
BASE_DISCOUNT = 5.0

calculate_total_spain = partial(calculate_total, TAX)
apply_fixed_discount = partial(apply_discount, BASE_DISCOUNT)

print(calculate_total_spain(100))  # 121.0

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:
        return price + (price * tax)

    def apply_discount(price: float) -> float:
        return price - discount

    def process_invoice(price: float) -> float:
        return apply_discount(calculate_total(price))

    return calculate_total, apply_discount, process_invoice


calculate, discount_fn, process = create_pricing_system(0.21, 5.0)

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.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Will you buy me a coffee?

Comments

There are no comments yet.

Written by Andros Fenollosa

June 5, 2026

2 min of reading

You may also like

Visitors in real time

You are alone: 🐱