Explain Codes LogoExplain Codes Logo

How to overload init method based on argument type?

python
class-factory-methods
metaclasses
function-annotations
Nikita BarsukovbyNikita Barsukov·Feb 19, 2025
TLDR
class MyClass: def __init__(self, arg): init_method = { str: self._handle_string, int: self._handle_int, }.get(type(arg), self._default_handler) # Because we also love our defaults, don't we? init_method(arg) def _handle_string(self, value): self.data = value # Storing strings, doing the string thing! def _handle_int(self, value): self.data = value * 2 # Integers? Double the fun! def _default_handler(self, value): self.data = None # Oops, didn't see that coming, did we? # Usage: instance_str = MyClass('hello') instance_int = MyClass(10)

Dictionaries are our magic spells. By mapping types to initializer methods, we simplify our type checking and make extension an easy task. Every possible input, handled within a method, something we like to call initiation strategy.

Method to our madness: Class factory methods

Sometimes, we want to speak with clarity. That's where class factory methods shine. These classmethods serve as convenient constructors providing an explicit interface:

class MyClass: @classmethod def from_string(cls, value): instance = cls() instance.data = value return instance @classmethod def from_int(cls, value): instance = cls() instance.data = value * 2 # Everyone loves a good multiplication! return instance # Usage: instance_str = MyClass.from_string('hello') instance_int = MyClass.from_int(10)

Handy, maintainable, no guesswork, and nothing but solid robust code. You're welcome!

Meta meets classes: Alternative Initialization

Move over basic overloading, say hello to metaclasses and function annotations:

class MultipleMeta(type): def __call__(cls, *args, **kwargs): annotations = cls.__init__.__annotations__ for name, arg_type in annotations.items(): if any(isinstance(arg, arg_type) for arg in args): return super().__call__(*args, **kwargs) raise TypeError("Well, that was unexpected.") class MyClass(metaclass=MultipleMeta): def __init__(self, value: [int, str]): if isinstance(value, int): self.data = value * 2 elif isinstance(value, str): self.data = value # Usage: instance_str = MyClass('world') instance_int = MyClass(42)

Metaclasses do the heavy lifting and pick the right initialization path, leaving our lovely __init__ method unharried.

Keep it simple, squire: Named Arguments for Simplicity

Named arguments, our knights in shining armor, simplify the overloading logic within the __init__ method. Just give your args default values for a one-method-fits-all approach:

class MyClass: def __init__(self, string_bean=None, the_number=None): if string_bean is not None: self.data = string_bean elif the_number is not None: self.data = the_number * 2 # There we go, doubling the fun again. Who says programmers don't have a sense of humor? else: raise ValueError("Whoopsie daisy, looks like we messed up the parameters!") # Usage: instance_str = MyClass(string_bean='python') instance_int = MyClass(the_number=10)

But remember, a gentleman's always clear about his intentions. Document your parameters.

No more guesswork: Handling Type Ambiguity

When type collision becomes a problem, use wrapper classes to break up the dogfight:

class StrWrapper: def __init__(self, value): self.value = value class MyClass: def __init__(self, arg): if isinstance(arg, StrWrapper): self.data = arg.value elif isinstance(arg, int): self.data = arg * 2 # Back at it with the multiplication! # Handle other mischievous types # Usage: instance_str = MyClass(StrWrapper('string')) instance_int = MyClass(10)

Does it take a bit extra work? Yes. Is it worth it? Absolutely!

There's more than one way to build a house: Dynamic initialization

Let's sophisticate our initialization process with dunder methods and single-dispatch generic functions:

from functools import singledispatchmethod class MyClass: def __init__(self, arg): self.set_data(arg) @singledispatchmethod def set_data(self, arg): raise NotImplementedError("We didn't see that coming!") @set_data.register def _(self, arg: int): self.data = arg * 2 @set_data.register def _(self, arg: str): self.data = arg # Usage: instance_generic = MyClass(20) instance_generic = MyClass('good morning')

Free your base __init__ method from the complexities of handling different initializations. See, living the swanky code life isn't too hard, isn't it?