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.
Using inspect.signature() (Recommended)
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
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
Parameter.kind attributeEach parameter has a kind attribute that tells you exactly what type it is:
| Kind | Description | Example |
|---|---|---|
POSITIONAL_OR_KEYWORD | Regular argument | def f(x): |
POSITIONAL_ONLY | Position-only (Python 3.8+) | def f(x, /): |
KEYWORD_ONLY | Keyword-only argument | def f(*, x): |
VAR_POSITIONAL | *args | def f(*args): |
VAR_KEYWORD | **kwargs | def 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')
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
*args and **kwargsClasses 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
| 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.