Python
Class Methods
Object-Oriented Programming
__init__
Function Calls

Calling a class function inside of __init__

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Calling a method from __init__ is a normal Python pattern, but it has tradeoffs that are easy to miss in larger codebases. The constructor runs during object creation, so any method you call there becomes part of object lifecycle guarantees. Good constructor design keeps initialization predictable, testable, and safe under inheritance.

What Happens When __init__ Calls a Method

Inside __init__, self already exists, so calling self.some_method() is valid. This is often used to normalize input, compute derived fields, or validate required values.

python
1class User:
2    def __init__(self, raw_name: str, age: int):
3        self.raw_name = raw_name
4        self.name = self._normalize_name(raw_name)
5        self.age = self._validate_age(age)
6
7    def _normalize_name(self, value: str) -> str:
8        return " ".join(part.capitalize() for part in value.strip().split())
9
10    def _validate_age(self, value: int) -> int:
11        if value < 0:
12            raise ValueError("age must be non-negative")
13        return value
14
15u = User("  ada lovelace  ", 28)
16print(u.name, u.age)

The constructor stays readable because domain logic is moved into helper methods.

Instance Method, Class Method, or Static Method

Developers often say class function when they mean any method defined in a class. In Python, there are three common method types with different constructor use cases.

  • Instance method: default option when logic depends on object state.
  • Class method: useful when logic depends on class-level configuration.
  • Static method: useful for pure helpers that need no class or instance state.
python
1class Token:
2    prefix = "tok"
3
4    def __init__(self, raw: str):
5        self.value = self._build(raw)
6
7    @classmethod
8    def _build(cls, raw: str) -> str:
9        return f"{cls.prefix}_{raw.strip().lower()}"
10
11    @staticmethod
12    def is_valid(raw: str) -> bool:
13        return raw.strip() != ""
14
15print(Token("ABC").value)
16print(Token.is_valid("x"))

This split clarifies intent and reduces accidental coupling.

Inheritance Risk: Overridable Methods in Constructors

The biggest hazard is calling a method that a subclass overrides. The base constructor may run before subclass attributes are ready.

python
1class Base:
2    def __init__(self):
3        self._prepare()  # can call subclass override
4
5    def _prepare(self):
6        pass
7
8class Child(Base):
9    def __init__(self):
10        self.threshold = 10
11        super().__init__()
12
13    def _prepare(self):
14        # If called before threshold exists, behavior breaks
15        print(self.threshold)

This pattern is fragile because constructor ordering is easy to change accidentally during refactors.

A safer design is to avoid calling overridable methods in base constructors. Use private helpers on each class, or defer extension hooks to a separate explicit method.

Safer Patterns for Complex Initialization

Use a small constructor and explicit helper sequencing.

python
1class Config:
2    def __init__(self, env: dict[str, str]):
3        self.env = env
4        self.host = ""
5        self.port = 0
6        self.debug = False
7        self._load()
8        self._validate()
9
10    def _load(self) -> None:
11        self.host = self.env.get("HOST", "localhost")
12        self.port = int(self.env.get("PORT", "8080"))
13        self.debug = self.env.get("DEBUG", "0") == "1"
14
15    def _validate(self) -> None:
16        if not (1 <= self.port <= 65535):
17            raise ValueError("PORT must be between 1 and 65535")
18
19cfg = Config({"HOST": "api.local", "PORT": "9000", "DEBUG": "1"})
20print(cfg.host, cfg.port, cfg.debug)

This approach makes object creation deterministic and easier to test in isolation.

Dataclasses and __post_init__

For dataclasses, place post-construction logic in __post_init__ instead of a custom constructor unless you need full control.

python
1from dataclasses import dataclass
2
3@dataclass
4class Product:
5    name: str
6    price_cents: int
7
8    def __post_init__(self):
9        self.name = self.name.strip().title()
10        if self.price_cents < 0:
11            raise ValueError("price_cents must be non-negative")
12
13p = Product("  keyboard  ", 4999)
14print(p)

You keep dataclass benefits while still running validation and normalization.

When a Factory Is Better Than a Constructor Call Chain

If initialization needs I/O, retries, or alternate creation modes, a factory class method is often clearer than stacking many constructor helper calls.

python
1class Connection:
2    def __init__(self, host: str, port: int):
3        self.host = host
4        self.port = port
5
6    @classmethod
7    def from_env(cls, env: dict[str, str]) -> "Connection":
8        host = env.get("DB_HOST", "localhost")
9        port = int(env.get("DB_PORT", "5432"))
10        return cls(host, port)
11
12conn = Connection.from_env({"DB_HOST": "db", "DB_PORT": "5432"})
13print(conn.host, conn.port)

Factories keep constructors simple and make alternative creation paths explicit.

Common Pitfalls

  • Calling overridable methods from a base __init__. Fix: avoid override hooks in constructors or ensure all needed state exists first.
  • Doing network or disk I/O during construction. Fix: move heavy work to explicit methods or factories so failures are easier to handle.
  • Hiding too much logic behind long helper chains. Fix: keep initialization flow short and obvious.
  • Mixing validation and side effects. Fix: validate first, then perform side effects in separate steps.
  • Assuming class methods and instance methods are interchangeable. Fix: choose method type based on whether logic depends on class state or object state.

Summary

  • Calling methods inside __init__ is valid and often useful.
  • Prefer small constructor helpers for normalization and validation.
  • Avoid constructor calls to methods that subclasses can override.
  • Use __post_init__ with dataclasses for post-processing logic.
  • Use factory methods when construction requires complex setup paths.

Course illustration
Course illustration

All Rights Reserved.