Skip to main content

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

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__)
# Exclude 'self' from the count
return len(sig.parameters) - 1

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

Output:

Number of explicit arguments: 3
note

inspect.signature() parses the method's parameter list and returns a Signature object. The .parameters attribute is an ordered dictionary of parameter names to Parameter objects. We subtract 1 to exclude self.

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
The Parameter.kind attribute

Each parameter has a kind attribute that tells you exactly what type it is:

KindDescriptionExample
POSITIONAL_OR_KEYWORDRegular argumentdef f(x):
POSITIONAL_ONLYPosition-only (Python 3.8+)def f(x, /):
KEYWORD_ONLYKeyword-only argumentdef f(*, x):
VAR_POSITIONAL*argsdef f(*args):
VAR_KEYWORD**kwargsdef f(**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__)

# spec.args includes 'self', so subtract 1
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__

# co_argcount includes 'self'
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')
note

The __code__ approach works at the bytecode level and does not account for *args or **kwargs in co_argcount. For comprehensive parameter analysis, use inspect.signature() instead.

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
Watch out for *args and **kwargs

Classes that use *args and **kwargs in their __init__ can accept any number of arguments. The explicit argument count won't reflect this:

import inspect

class Flexible:
def __init__(self, name, *args, **kwargs):
pass

sig = inspect.signature(Flexible.__init__)
params = [p for p in sig.parameters.values() if p.name != 'self']
print(f"Explicit parameters: {len(params)}")
# Output: Explicit parameters: 3 (however, the class can accept any number of arguments)

Check each parameter's kind to determine if *args or **kwargs are present.

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

MethodDetail LevelHandles *args/**kwargsBest 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.