Skip to content

How to Get the Number of Explicit Arguments in the __init__ of a Class in Python

Learn how to programmatically count explicit arguments (excluding self) in a Python class's __init__ method using inspect.signature(), getfullargspec(), and __code__ for framework development, ORMs, and debugging.

In Python, the __init__ method initializes newly created objects and typically accepts parameters that define the object's initial state. There are situations, such as building frameworks, serializers, ORMs, or debugging tools, where you need to programmatically determine how many explicit arguments a class's __init__ method expects (excluding self).

This guide demonstrates several approaches to count these arguments using Python's built-in introspection capabilities.

The inspect module provides signature(), which returns a detailed representation of a function's parameters. This is the most reliable and Pythonic approach.

import inspect

class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age

def count_init_args(cls):
    """Count explicit arguments in __init__, excluding 'self'."""
    sig = inspect.signature(cls.__init__)
    return len(sig.parameters) - 1

print(f"Number of explicit arguments: {count_init_args(User)}")

Output:

Number of explicit arguments: 3

Handling Different Parameter Types

The inspect module can also distinguish between different kinds of parameters (*args, **kwargs, keyword-only, etc.):

import inspect

class ComplexClass:
    def __init__(self, required, optional="default", *args, keyword_only=True, **kwargs):
        pass

def analyze_init_params(cls):
    """Analyze all parameter types in __init__."""
    sig = inspect.signature(cls.__init__)
    
    params = {
        "positional": [],
        "keyword_only": [],
        "var_positional": None,   # *args
        "var_keyword": None       # **kwargs
    }
    
    for name, param in sig.parameters.items():
        if name == "self":
            continue
        if param.kind == inspect.Parameter.VAR_POSITIONAL:
            params["var_positional"] = name
        elif param.kind == inspect.Parameter.VAR_KEYWORD:
            params["var_keyword"] = name
        elif param.kind == inspect.Parameter.KEYWORD_ONLY:
            params["keyword_only"].append(name)
        else:
            params["positional"].append(name)
    
    return params

result = analyze_init_params(ComplexClass)
print("Positional args:", result["positional"])
print("Keyword-only args:", result["keyword_only"])
print("*args name:", result["var_positional"])
print("**kwargs name:", result["var_keyword"])

Output:

Positional args: ['required', 'optional']
Keyword-only args: ['keyword_only']
*args name: args
**kwargs name: kwargs

Using inspect.getfullargspec()

The getfullargspec() function returns a named tuple with detailed information about a function's arguments, including defaults, annotations, and keyword-only parameters:

import inspect

class Product:
    def __init__(self, name, price, category="general"):
        self.name = name
        self.price = price
        self.category = category

spec = inspect.getfullargspec(Product.__init__)

explicit_count = len(spec.args) - 1

print(f"All args (including self): {spec.args}")
print(f"Explicit arguments: {explicit_count}")
print(f"Defaults: {spec.defaults}")

Output:

All args (including self): ['self', 'name', 'price', 'category']
Explicit arguments: 3
Defaults: ('general',)

Counting Only Required Arguments

You can determine how many arguments are required (have no default value) by comparing the total count against the number of defaults:

import inspect

class Config:
    def __init__(self, host, port, debug=False, timeout=30):
        pass

def count_required_args(cls):
    """Count only required (non-default) arguments in __init__."""
    spec = inspect.getfullargspec(cls.__init__)
    total_args = len(spec.args) - 1  # Exclude 'self'
    num_defaults = len(spec.defaults) if spec.defaults else 0
    return total_args - num_defaults

print(f"Total explicit args: {len(inspect.getfullargspec(Config.__init__).args) - 1}")
print(f"Required args only: {count_required_args(Config)}")

Output:

Total explicit args: 4
Required args only: 2

Using __code__ for Low-Level Access

Every function in Python has a __code__ attribute that provides direct access to the compiled bytecode information, including argument counts:

class Vehicle:
    def __init__(self, make, model, year, color="black"):
        pass

code = Vehicle.__init__.__code__

total = code.co_argcount - 1
var_names = code.co_varnames[1:code.co_argcount]  # Skip 'self'

print(f"Explicit arguments: {total}")
print(f"Argument names: {var_names}")

Output:

Explicit arguments: 4
Argument names: ('make', 'model', 'year', 'color')

Handling Edge Cases

Classes Without a Custom __init__

If a class doesn't define its own __init__, it inherits from object, which takes no explicit arguments:

import inspect

class Empty:
    pass

def count_init_args(cls):
    sig = inspect.signature(cls.__init__)
    params = [p for p in sig.parameters.values() if p.name != 'self']
    return len(params)

print(f"Empty class args: {count_init_args(Empty)}")

Output:

Empty class args: 2

Inherited __init__

The function inspects whichever __init__ is resolved through the Method Resolution Order (MRO):

import inspect

class Base:
    def __init__(self, x, y):
        pass

class Child(Base):
    pass  # Inherits Base.__init__

class Override(Base):
    def __init__(self, a, b, c):
        super().__init__(a, b)

def count_init_args(cls):
    sig = inspect.signature(cls.__init__)
    return len(sig.parameters) - 1

print(f"Base args: {count_init_args(Base)}")
print(f"Child args (inherited): {count_init_args(Child)}")
print(f"Override args: {count_init_args(Override)}")

Output:

Base args: 2
Child args (inherited): 2
Override args: 3

Practical Example: Auto-Documentation

Here's a real-world example that generates documentation for class constructors:

import inspect

def document_init(cls):
    """Generate documentation for a class's __init__ method."""
    sig = inspect.signature(cls.__init__)
    params = []
    
    for name, param in sig.parameters.items():
        if name == 'self':
            continue
        info = {"name": name, "kind": param.kind.name}
        if param.default is not inspect.Parameter.empty:
            info["default"] = param.default
        if param.annotation is not inspect.Parameter.empty:
            info["type"] = param.annotation.__name__
        params.append(info)
    
    print(f"Class: {cls.__name__}")
    print(f"Total explicit parameters: {len(params)}")
    for p in params:
        parts = [f"  - {p['name']}"]
        if "type" in p:
            parts.append(f"(type: {p['type']})")
        if "default" in p:
            parts.append(f"[default: {p['default']}]")
        print(" ".join(parts))

class DatabaseConnection:
    def __init__(self, host: str, port: int, database: str, 
                 timeout: int = 30, ssl: bool = True):
        pass

document_init(DatabaseConnection)

Output:

Class: DatabaseConnection
Total explicit parameters: 5
  - host (type: str)
  - port (type: int)
  - database (type: str)
  - timeout (type: int) [default: 30]
  - ssl (type: bool) [default: True]

Comparison of Approaches

Method Detail Level Handles *args/**kwargs Best For
inspect.signature() ⭐⭐⭐⭐⭐ ✅ Most use cases (recommended)
inspect.getfullargspec() ⭐⭐⭐⭐ ✅ When you need defaults separately
__code__.co_argcount ⭐⭐ ❌ Low-level, performance-critical code
__annotations__ ⭐⭐ ❌ Only works with annotated params

Conclusion

The inspect.signature() function is the most robust and recommended way to count explicit arguments in a class's __init__ method. It handles all parameter types, including regular, default, keyword-only, *args, and **kwargs, and provides rich metadata for each parameter.

Use getfullargspec() when you need defaults and annotations as separate collections, and reserve the __code__ approach for low-level performance scenarios.

Understanding these introspection tools enables you to build more dynamic, self-documenting, and framework-friendly Python code.

Navigation