2026, Jan 13 13:00

Python Pattern: Dynamic Method Default Arguments per Subclass with __init_subclass__ for DRY Signatures

Make method default arguments dynamic per subclass in Python with __init_subclass__, so class attribute overrides update the signature. DRY, readable approach.

Making method defaults dynamic per subclass looks deceptively simple in Python, yet it trips up even experienced developers. The recurring wish: define a default parameter based on a class attribute, override that attribute in a subclass, and have the method inherit the new default without rewriting the method. The goal is DRY, readable, and predictable.

Problem overview

The intent is straightforward: define a class-level constant and use it as the default value in a method. Then override the constant in a subclass and expect the method to automatically pick up the new default. Here is the shape of that pattern:

class BaseCountry:  # was A
FALLBACK_TEXT = "Unspecified Country" # was DEFAULT_MESSAGE

def run(self, msg=FALLBACK_TEXT): # was do_it(message=DEFAULT_MESSAGE)
print(msg)

class MexicoCountry(BaseCountry): # was B
FALLBACK_TEXT = "Mexico"

# Desired behavior when calling:
BaseCountry().run() # Unspecified Country
MexicoCountry().run() # Mexico

Without additional machinery, overriding the class attribute does not update the default parameter value in the inherited method. A common workaround is to pass None by default and read the class attribute inside the method body, but not everyone is happy with how that hides the default from the signature.

Why this happens

Python does not re-evaluate method interfaces on subclass creation. In other words, when a method is defined on the base class, its default values are set on that function object and do not automatically change just because a subclass overrides a class attribute. The interface remains the same unless the subclass redefines the method itself.

A practical, DRY approach using __init_subclass__

A workable technique is to use __init_subclass__ to intervene at subclass creation time. The idea is to copy the function object, adjust its defaults to point at the subclass’s attribute, and then attach that adjusted function to the subclass. This keeps the method body DRY while aligning the signature with each subclass’s attribute. The trade-off is that it introduces a bit of “magic,” so use it only when the clarity and ergonomics justify the indirection.

def _clone_callable(fn):  # was _copy_func
import types
import functools
new_fn = types.FunctionType(fn.__code__, fn.__globals__, fn.__name__, fn.__defaults__)
new_fn = functools.update_wrapper(new_fn, fn)
new_fn.__kwdefaults__ = fn.__kwdefaults__
return new_fn

class CountryBase: # was A
TEXT_FALLBACK = "Unspecified Country" # was DEFAULT_MESSAGE

def run(self, message=TEXT_FALLBACK): # was do_it
print(message)

def __init_subclass__(cls, **kw):
super().__init_subclass__(**kw)
adjusted = _clone_callable(cls.run)
adjusted.__defaults__ = (cls.TEXT_FALLBACK,)
cls.run = adjusted

class CountryMX(CountryBase): # was B
TEXT_FALLBACK = "Mexico"

# Calls:
CountryBase().run() # Unspecified Country
CountryMX().run() # Mexico

This approach modifies the inherited method’s defaults for each subclass at the moment the subclass is created. It is effective and keeps the method implementation in one place. It does, however, rely on knowing which methods to adjust and on having access to the appropriate attribute names. The same pattern can be extended by introspecting signature details or by explicitly marking which methods should be adjusted.

What about decorators?

The same underlying theme appears with class or method decorators that one might wish to “re-evaluate” for each subclass. If a decorator relies on class-level data, it will not automatically rerun for subclasses in the way you might expect. The situation is conceptually aligned with the default-argument scenario discussed above.

Why this matters

APIs are read as much as they are executed. When defaults reflect class-level configuration directly in the signature, it keeps the contract visible. Some prefer not to hide the source of truth inside the method body. Others are comfortable with the idiomatic None pattern and consider it perfectly acceptable. Both positions are valid; the key is to be consistent and choose the approach that matches the team’s expectations and codebase norms. The __init_subclass__ technique offers an option when signature clarity and DRYness are priorities, with the caveat that it introduces indirection that not everyone will want in production.

Recommendations

Use the simplest viable approach for your codebase. If the None sentinel is acceptable to your team, it remains a practical and familiar option. If you need the signature itself to reflect subclass-specific defaults without duplicating method bodies, the __init_subclass__ pattern shown above provides a contained and explicit mechanism. Be mindful that it feels a bit “magical,” so document the pattern where it’s used and keep the surface area small.