Two places in a Python class can legally hold a docstring: right under the class line, and right under def __init__. They are not interchangeable, and the style guides do not all agree on which one should hold constructor parameter documentation. Understanding what each source says — rather than what is commonly assumed — changes how you write and read Python documentation.
New Python programmers often place documentation wherever it seems to fit and move on. That works until someone calls help() on your Python class and finds half the information they expected is missing or buried in the wrong place. The distinction between a class docstring and an __init__ docstring is a small thing that has a visible effect — and the official guidance is more nuanced than many tutorials let on.
What each docstring actually documents
A class docstring describes the class itself: what it represents, what it is for, and how it behaves as a whole. An __init__ docstring, if it exists at all, describes the constructor method specifically — the parameters needed to create an instance.
Think of it this way. The class docstring answers the question "what is this thing?" The __init__ docstring answers "what do I pass in to build one?" Those are related questions, but they are not the same question.
class NetworkDevice:
"""Represents a managed network device in the inventory system.
Tracks connection state, IP address, and device type. Use
DeviceManager to query and update collections of devices.
"""
def __init__(self, hostname: str, ip: str, device_type: str = "router"):
self.hostname = hostname
self.ip = ip
self.device_type = device_type
In many cases, this is all you need. The class docstring describes the object; the constructor's parameters speak for themselves through type hints and clear names. You do not always need an __init__ docstring at all.
A docstring is only a docstring when it is the first statement inside a block — a string literal with nothing before it except the def or class line itself. A string placed after any other statement is just an expression that does nothing.
What PEP 257 actually says
PEP 257 is Python's official docstring convention guide, authored by David Goodger and Ken Manheimer. It was adapted from Guido van Rossum's original Python style guide essay. Its guidance on class constructors is explicit and frequently misquoted.
PEP 257 states that the class constructor should be documented in the __init__ method's docstring — not in the class-level docstring. — PEP 257, authored by David Goodger and Ken Manheimer
This is the opposite of what many tutorials claim. PEP 257 routes constructor argument documentation to __init__, not to the class docstring. The class-level docstring has a separate purpose: PEP 257 says it should summarize the class's behavior and list public methods and instance variables. These are two different documentation jobs, and PEP 257 assigns them to two different docstrings.
The confusion likely has two sources. First, NumPy-style projects put constructor params in the class docstring — that pattern is so visible in the scientific Python ecosystem that many developers assume it is the universal rule. Second, some older tutorials paraphrase PEP 257 inaccurately, dropping the word "__init__" and writing something like "class constructors are documented in the class docstring," which is the opposite of what the spec says.
PEP 257 also specifies that __init__ is a public method and, like all public methods, should have its own docstring. The class docstring is not a substitute for it. PEP 8, the general Python style guide, reinforces this: it states that docstrings are not necessary for non-public methods, which means the inverse — public methods including __init__ should have them. (PEP 8, peps.python.org)
# PEP 257 style — class summarizes behavior, __init__ documents parameters
class FirewallRule:
"""A single allow/deny rule applied to an interface.
Tracks action, protocol, and port. Attach to an Interface
object via interface.add_rule() to take effect.
"""
def __init__(self, action: str, protocol: str, port: int):
"""Initialize the rule with action, protocol, and port.
Args:
action (str): Either 'allow' or 'deny'.
protocol (str): Network protocol, e.g. 'tcp' or 'udp'.
port (int): Destination port number.
"""
self.action = action
self.protocol = protocol
self.port = port
Tools like pydocstyle and flake8-docstrings can lint your docstrings against PEP 257 automatically. Run pydocstyle --convention=pep257 yourfile.py to check compliance — especially useful in team projects where consistency matters.
How help() and __doc__ behave
This is where placement has a concrete, observable effect. When you call help() on a class, Python displays ClassName.__doc__, which is the class-level docstring. It also shows each method's docstring, including __init__.__doc__. Both are shown, but the class docstring appears at the top and sets the tone for everything below it.
Notice that accessing ClassName.__doc__ returns only the class-level string. The __init__ docstring is entirely separate and lives on the method object. If you put all your documentation inside __init__ and leave the class docstring blank, then MyClass.__doc__ returns None and any tool that reads the class description finds nothing.
Sphinx, pdoc, and similar documentation generators read __doc__ directly. A class with no class-level docstring produces a documentation page with no description, even if __init__ is thoroughly documented.
What the CPython runtime actually does with your docstring
Understanding the mechanics helps explain why placement decisions are permanent once a class is imported. This is the part most Python documentation articles skip entirely.
When CPython compiles a class body, it looks for the first expression statement — a bare string literal that appears before any assignment or other statement. If it finds one, the compiler stores it as a constant and the bytecode emits a STORE_NAME instruction that assigns it to __doc__. The same logic applies independently inside the __init__ body. The two compilation passes are completely independent of each other. Nothing in the compiler coordinates them.
This means ClassName.__doc__ and ClassName.__init__.__doc__ are set at separate times, in separate code objects, with no cross-reference between them. They are not two views of the same data. They are two distinct string constants attached to two distinct objects — the class object and the function object respectively.
Sphinx's autoclass directive reads ClassName.__doc__ by default for the class description. To also render the __init__ docstring, you have to pass :special-members: __init__ explicitly — or use autoclass_content = 'both' in conf.py. If your project uses the default Sphinx setup and your class docstring is empty, your published documentation has no class description even if __init__ is fully documented. This is not a Sphinx limitation. It is a direct consequence of how CPython stores docstrings.
There is one more subtlety worth knowing. Python's help() function — technically pydoc.help() — does not simply display __doc__. It uses the inspect module to walk the class's MRO, then formats the class docstring, each method's docstring, and inherited members. For __init__ specifically, pydoc uses a heuristic: if __init__.__doc__ is identical to object.__init__.__doc__ (the base object constructor's docstring), it suppresses the __init__ section entirely in the output. This means a class that inherits __init__ from object and adds no override will show nothing in the init section of help() output — which is the correct behavior.
The practical lesson: check what help(YourClass) actually produces in your terminal before shipping documentation. The output may surprise you. A class with both docstrings set correctly, including a real __init__.__doc__ that is not just the inherited placeholder, will render both sections. A class where only the class docstring exists will show the class description but nothing under the Constructor heading. A class with only an __init__ docstring will show the init docs but present a blank class description — which is the common mistake documented throughout this article.
# Verify docstring storage — run this in your own REPL
class Demo:
"""Class-level docstring — stored as Demo.__doc__."""
def __init__(self):
"""__init__ docstring — stored as Demo.__init__.__doc__."""
# Two separate objects, two separate strings:
print(Demo.__doc__) # Class-level docstring — stored as Demo.__doc__.
print(Demo.__init__.__doc__) # __init__ docstring — stored as Demo.__init__.__doc__.
# Deleting one does not affect the other:
Demo.__doc__ = None
print(Demo.__doc__) # None
print(Demo.__init__.__doc__) # __init__ docstring — still intact
Google style and NumPy style compared
Here is where things diverge from both PEP 257 and from each other. Google style and NumPy style each take a meaningfully different approach, and neither works the way many tutorials describe.
Section 3.8.4 of the Google Python Style Guide is explicit about what each docstring holds. It specifies that classes should carry a docstring beneath the class definition, and that public attributes (excluding properties) belong in an Attributes section of that class docstring — which the guide describes as the place where "public attributes, excluding properties, should be documented." (Google Python Style Guide §3.8.4, google.github.io)
That same section then shows an __init__ docstring with an Args section for constructor parameters. The two docstrings coexist — they are not alternatives, they document separate things.
The numpydoc specification makes its class-docstring-first approach explicit. On the topic of class documentation, the guide states:
"a docstring for the class constructor (__init__) can, optionally, be added" — numpydoc Style Guide, numpydoc.readthedocs.io
In other words, the numpydoc guide routes constructor parameters to the class docstring's Parameters section as the primary location, and explicitly marks any __init__ docstring as optional supplemental detail — only warranted when additional initialization context is needed beyond what the class docstring already covers.
This means the numpydoc approach is not "pick one and be consistent." It is: put constructor params in the class docstring's Parameters section, and only write an __init__ docstring if you need to document something beyond what is already there. The numpydoc validation tooling enforces this: a well-formed class docstring suppresses the GL08 warning that would otherwise flag a missing __init__ docstring.
ClassName.__doc__ returns None
When does __init__ not need a docstring at all?
The article has noted this once without explaining it, so it deserves a direct answer. An __init__ docstring adds real value when one or more of the following is true:
- A parameter has non-obvious behavior — a default that changes under certain conditions, a constraint not expressed in the type hint, or a side effect on initialization.
- The parameter name alone is ambiguous.
timeout: intraises the question "timeout of what, in what unit?" A docstring resolves that.hostname: strdoes not raise that question. - Your project follows PEP 257 or Google style, both of which treat
__init__as a public method that always warrants its own docstring.
When parameters are few, well-named, and accurately type-hinted, and when the class docstring already explains how to instantiate the object, a minimal or absent __init__ docstring is often cleaner than a redundant one. NumPy-style projects commonly operate this way by design. The decision is not laziness — it is a judgment that the code and type hints already communicate what a docstring would only repeat.
If you are writing an __init__ docstring that only restates the parameter names and types already in the signature, delete it. Use that space to document the constraint, the unit, or the side effect — the thing the signature cannot tell you.
Where does a Raises section go?
Constructors raise exceptions. A ValueError when a parameter falls outside an accepted range, a TypeError when the wrong type is passed, a FileNotFoundError when a path argument does not resolve — these are real behaviors a caller needs to know about. The article has covered where Args and Parameters go. It has not answered where Raises goes, and the answer differs by style.
Under both PEP 257 and Google style, the Raises section belongs in the __init__ docstring, directly alongside Args. The logic is consistent with the rest of those conventions: constructor behavior — including failure modes — belongs in the constructor's own docstring. The class docstring describes what the class is; the __init__ docstring describes what can happen when you try to build one.
Under NumPy / numpydoc style, the Raises section goes in the class docstring, immediately after the Parameters section. Since the class docstring is already doing the work of documenting constructor parameters, it also carries the failure documentation. The numpydoc section ordering is: Summary, Extended summary, Parameters, Raises, Attributes, Notes — with Raises appearing before Attributes when both are present.
# Google style — Raises in __init__ docstring alongside Args
class PacketFilter:
"""Applies BPF-style filtering to captured network packets.
Attributes:
rule (str): The active filter expression.
is_compiled (bool): True once the rule has been compiled.
"""
def __init__(self, rule: str):
"""Initialize the filter with a BPF expression.
Args:
rule (str): A valid BPF filter expression, e.g. 'tcp port 443'.
Raises:
ValueError: If rule is an empty string.
ValueError: If rule contains characters not valid in a BPF expression.
"""
if not rule.strip():
raise ValueError("Filter rule cannot be empty.")
self.rule = rule
self.is_compiled = False
The NumPy equivalent puts both the Parameters and Raises sections in the class docstring, leaving __init__ with no docstring at all:
# NumPy style — Parameters and Raises both in class docstring
class PacketFilter:
"""Applies BPF-style filtering to captured network packets.
Parameters
----------
rule : str
A valid BPF filter expression, e.g. 'tcp port 443'.
Raises
------
ValueError
If rule is an empty string.
ValueError
If rule contains characters not valid in a BPF expression.
Attributes
----------
is_compiled : bool
True once the rule has been compiled.
"""
def __init__(self, rule: str):
if not rule.strip():
raise ValueError("Filter rule cannot be empty.")
self.rule = rule
self.is_compiled = False
Only document exceptions a caller can realistically handle or anticipate. An uncaught MemoryError during deep initialization does not belong in a Raises section. A ValueError you explicitly raise for an invalid argument does.
Classmethod alternative constructors
A common Python pattern is a class that exposes multiple construction paths through @classmethod factory methods — from_dict, from_file, from_env, and so on. These are fully public methods that accept parameters and return an instance. Where does their documentation go?
The answer is the same across all three conventions: each @classmethod constructor gets its own docstring, written directly under its def line, the same way any other public method would be documented. The class docstring describes the class as a whole. It does not need to enumerate every factory method and its parameters — that would duplicate information and create a maintenance problem. What belongs in the class docstring is a note that alternative constructors exist, so a reader knows to look.
from typing import Self # typing.Self requires Python 3.11+
class ThreatFeed:
"""A parsed collection of threat indicators from an external feed source.
Use ThreatFeed(indicators) for direct construction or one of the
class methods (from_stix, from_csv) to load from a file format.
"""
def __init__(self, indicators: list[dict]):
"""Initialize the feed with a pre-parsed list of indicators.
Args:
indicators (list[dict]): A list of indicator dicts, each with
'type', 'value', and 'confidence' keys.
"""
self.indicators = indicators
@classmethod
def from_stix(cls, path: str) -> Self:
"""Load a threat feed from a STIX 2.1 JSON bundle file.
Args:
path (str): Filesystem path to the .json bundle.
Returns:
Self: A new instance populated from the STIX bundle.
Raises:
FileNotFoundError: If path does not exist.
ValueError: If the file is not a valid STIX 2.1 bundle.
"""
# ... parsing logic ...
return cls([])
@classmethod
def from_csv(cls, path: str, delimiter: str = ",") -> Self:
"""Load a threat feed from a CSV file.
Args:
path (str): Filesystem path to the .csv file.
delimiter (str): Column separator. Defaults to ','.
Returns:
Self: A new instance populated from the CSV rows.
Raises:
FileNotFoundError: If path does not exist.
"""
# ... parsing logic ...
return cls([])
Notice that the class docstring mentions from_stix and from_csv exist, but does not repeat their parameter lists. That information lives where tools will find it: on each method's own docstring. Sphinx's autoclass directive renders class and method docstrings together, so the reader gets the full picture without duplication in the source.
The Returns section is relevant here even though a constructor normally returns None. Because a @classmethod factory method explicitly returns an instance, documenting the return type is not optional. Both Google and NumPy styles have a Returns section for exactly this situation.
Three cases where the standard advice breaks down
PEP 257 and the style guides assume a normal class with a standard __init__. Three patterns come up regularly in Python code where that assumption does not hold, and the usual guidance either becomes ambiguous or produces a result you probably did not intend.
Dataclasses
The @dataclass decorator generates __init__ automatically from the class-level field definitions. There is no def __init__ for you to write a docstring on. The generated method does have a __doc__ attribute, but it is set by the decorator and not something you can annotate directly without overriding the constructor entirely — which defeats the point of using a dataclass.
The practical answer for dataclasses is to document everything in the class docstring. Field descriptions go there, constructor intent goes there, and the class docstring carries the entire load. Tools like Sphinx with the autodoc extension can render field-level documentation from type annotations and inline comments, so the class docstring does not need to repeat every field name — it describes the object's purpose and notes anything the field definitions do not make obvious.
from dataclasses import dataclass, field
@dataclass Generates __init__ — no location to add a constructor docstring
class ThreatIndicator:
"""A single indicator of compromise attached to a threat report.
All fields are required except tags, which defaults to an empty list.
The ioc_value should be normalized to lowercase before assignment;
matching logic elsewhere is case-sensitive.
"""
ioc_type: str # e.g. "ip", "domain", "file_hash"
ioc_value: str
confidence: int # 0–100 scale
tags: list[str] = field(default_factory=list)
There is no __init__ docstring to write here. The class docstring is the only place to put documentation that affects how someone instantiates the object.
One wrinkle: if the dataclass defines a __post_init__ method to handle validation or computed fields after the generated __init__ runs, that method should have its own docstring. __post_init__ is a named, callable method that a developer may encounter directly — in debugging output, in a subclass, or when overriding it. Unlike __init__, it is not generated automatically. There is a real def line to attach a docstring to, and if it raises exceptions or performs non-obvious transformations, those behaviors belong in that docstring.
from dataclasses import dataclass, field
@dataclass
class ThreatIndicator:
"""A single indicator of compromise attached to a threat report.
All fields are required except tags, which defaults to an empty list.
ioc_value is normalized to lowercase on initialization.
"""
ioc_type: str
ioc_value: str
confidence: int
tags: list[str] = field(default_factory=list)
def __post_init__(self):
"""Normalize fields and validate after __init__ runs.
Raises:
ValueError: If confidence is outside the 0–100 range.
"""
self.ioc_value = self.ioc_value.lower()
if not 0 <= self.confidence <= 100:
raise ValueError(f"confidence must be 0–100, got {self.confidence}")
Subclasses that inherit __init__
When a subclass inherits __init__ without overriding it, SubClass.__init__.__doc__ returns the parent's docstring — or None if the parent had none. That can mislead a caller who reads the subclass documentation and finds constructor parameters that belong to a different class context.
The cleaner approach is to write a class docstring on the subclass that describes what makes it distinct, and to note that constructor parameters are inherited. Sphinx's :inherited-members: flag handles the generated documentation side automatically.
class BaseScanner:
"""Base class for network scanning operations."""
def __init__(self, target: str, timeout: int = 10):
"""Initialize the scanner against a target.
Args:
target (str): Hostname or CIDR range to scan.
timeout (int): Per-host timeout in seconds. Defaults to 10.
"""
self.target = target
self.timeout = timeout
class PortScanner(BaseScanner):
"""Scanner that checks open TCP ports on a target host.
Inherits constructor from BaseScanner. Use the ports parameter
on scan() to restrict the range checked.
"""
# No __init__ override — BaseScanner.__init__.__doc__ is inherited.
# The class docstring describes what is distinct about this subclass.
Classes that use __new__ instead of __init__
Some classes control instantiation through __new__ rather than __init__ — singletons, immutable types, and factory patterns that need to return a different type are common examples. PEP 257 is silent on __new__. The docstring conventions for __init__ do not transfer automatically.
The working convention is to treat __new__ like any other public method: give it its own docstring that documents the parameters it accepts, and give the class a docstring that explains the instantiation pattern. If __new__ is doing work that callers need to understand — such as enforcing a singleton or returning a cached instance — that belongs in the class docstring, not buried in __init__.
class ConfigManager:
"""Singleton that holds the active application configuration.
Only one instance exists per process. Subsequent calls to
ConfigManager() return the same object without re-reading the
config file.
"""
_instance = None
def __new__(cls, config_path: str = "/etc/app/config.yaml") -> "ConfigManager":
"""Return the existing instance or create one from config_path.
Args:
config_path (str): Path to the YAML configuration file.
Only used on first instantiation.
Returns:
ConfigManager: The singleton instance, newly created or cached.
"""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._load(config_path)
return cls._instance
NamedTuple and TypedDict
typing.NamedTuple and typing.TypedDict are two patterns where docstring placement is genuinely non-obvious and rarely covered in documentation guides.
A class-based NamedTuple definition uses a class body with field annotations, and the class docstring placement works normally — you write it immediately after the class line. The subtlety is that NamedTuple generates __new__ rather than __init__ for construction (tuples are immutable; allocation happens in __new__). There is no hand-written __init__ to document. Treat it exactly like a dataclass: the class docstring carries the full documentation load, including field descriptions.
from typing import NamedTuple
class TLSCert(NamedTuple):
"""An immutable TLS certificate record parsed from a PEM file.
Fields can be passed positionally or by keyword — TLSCert(hostname, expiry, issuer)
or TLSCert(hostname='example.com', expiry='2027-03-01', issuer='Let\'s Encrypt').
Construction uses __new__, not __init__, so all documentation
belongs here in the class docstring.
"""
hostname: str
expiry: str # ISO 8601 date string, e.g. '2027-03-01'
issuer: str
TypedDict is different in a more fundamental way: instances are plain dict objects, not class instances. You never call TypedDict to construct an object the way you call a class — the "constructor" is just a dict literal that happens to be type-checked. There is no __init__, no __new__ that you interact with, and nothing to document in that sense. The class docstring should explain what the dict structure represents and note any required vs optional keys (those with total=False). Tools like mypy read the annotations; the docstring is for human readers.
from typing import TypedDict, NotRequired # NotRequired requires Python 3.11+; use typing_extensions for 3.9–3.10
class ScanResult(TypedDict):
"""Shape of a single port scan result dict.
This is a TypedDict — instances are plain dicts, not class objects.
There is no __init__ to document. 'cve_ids' is optional and
defaults to absent (not an empty list) when no CVEs are found.
"""
host: str
port: int
state: str # 'open', 'closed', or 'filtered'
cve_ids: NotRequired[list[str]]
Abstract base classes and abstractmethod
Abstract base classes introduce a documentation challenge that is easy to get wrong. When you define an @abstractmethod, you are specifying a contract — a method every subclass must implement. The question is: who documents the contract, the abstract base, or the concrete subclass?
The answer is the abstract base. The abstract method's docstring should describe what the method is supposed to do, what parameters it accepts, what it should return, and what exceptions it may raise — written as a specification, not an implementation. Concrete subclasses that implement the method without changing the contract can use a minimal docstring like """See base class.""", or simply omit the docstring and inherit the abstract version.
The class docstring on an abstract base class should also note that the class is not meant to be instantiated directly, and should list the abstract methods so a developer knows what they are required to implement.
from abc import ABC, abstractmethod
class ThreatScanner(ABC):
"""Abstract base for threat scanning engines.
Not for direct instantiation. Subclasses must implement scan().
The __init__ stores the target and is available to all subclasses.
"""
def __init__(self, target: str):
"""Store the scan target.
Args:
target (str): Hostname or IP address to scan.
"""
self.target = target
@abstractmethod
def scan(self) -> list:
"""Run the scan and return a list of findings.
Returns:
list: Each item is a dict with 'severity' and 'detail' keys.
Raises:
ConnectionError: If the target is unreachable.
"""
# Abstract — no implementation here
class NmapScanner(ThreatScanner):
"""ThreatScanner implementation using nmap as the backend."""
def scan(self) -> list:
"""See base class."""
# nmap implementation
return []
If a concrete subclass's implementation adds new parameters, raises new exceptions, or meaningfully changes the behavior described in the base class docstring, write a full docstring on the override. The rule of thumb: """See base class.""" is only appropriate when the contract is identical.
Documenting __init__ that calls super().__init__()
When a class calls super().__init__() and passes arguments up the chain, many developers ask whether the subclass docstring should repeat the parent's parameters. The answer depends on what the subclass changes.
If the subclass __init__ accepts all the same parameters as the parent and simply forwards them via super() — adding nothing — the Args section should document all parameters the caller must provide, even if they are ultimately handled by the parent. The caller does not call the parent constructor directly; they call the subclass. Saying "see parent class for parameters" is not helpful when the caller is looking at the subclass docstring to figure out what to pass.
If the subclass adds its own parameters on top of the parent's, document all of them in the subclass __init__ docstring. A note referencing the parent — "Additional args are passed to ParentClass.__init__" — is appropriate when **kwargs forwarding is involved. But do not drop parameters silently on the assumption the reader will cross-reference the MRO.
class AuthenticatedScanner(BaseScanner):
"""Scanner that uses API key authentication.
Extends BaseScanner with credential injection at construction.
"""
def __init__(self, target: str, api_key: str, timeout: int = 10):
"""Initialize the authenticated scanner.
Args:
target (str): Hostname or CIDR range to scan.
Forwarded to BaseScanner.__init__.
api_key (str): API key for the scanning service.
timeout (int): Per-host timeout in seconds. Defaults to 10.
Forwarded to BaseScanner.__init__.
"""
super().__init__(target, timeout)
self.api_key = api_key
Documenting *args and **kwargs in __init__
An __init__ that accepts **kwargs and forwards them to a parent or mixin is one of the harder documentation cases. The challenge is that the full set of accepted keyword arguments is not visible in the subclass signature — it depends on the MRO at runtime.
The pragmatic approach: document the arguments your class explicitly names. For the forwarded **kwargs, name the section clearly and describe what the kwargs are passed to, so the reader knows where to look for the rest. Do not pretend the **kwargs do not exist — that silently hides the interface.
For *args, the situation is usually cleaner because positional arguments have well-defined order. Document each positional argument by its index position and semantic meaning if the names are generic (args[0], args[1]), or use descriptive parameter names instead of *args wherever possible — then the annotation and name do the documentation work for you.
class PluginScanner(BaseScanner):
"""Scanner that loads a plugin chain at startup."""
def __init__(self, target: str, plugins: list, **kwargs):
"""Initialize the scanner with a plugin chain.
Args:
target (str): Hostname or CIDR range to scan.
plugins (list): Ordered list of plugin callables to apply
after each scan result.
**kwargs: Forwarded to BaseScanner.__init__. See BaseScanner
for supported keyword arguments (e.g. timeout).
"""
super().__init__(target, **kwargs)
self.plugins = plugins
Enum classes
Enum subclasses are a common pattern where normal constructor documentation guidance does not apply. You do not call MyEnum(value) the way you call a regular class — you access members by name (MyEnum.MEMBER) or look them up by value (MyEnum(value)). The __init__ in an Enum class serves a different purpose: if defined, it initializes member attributes, not the member itself.
For a plain Enum with no custom __init__, the class docstring describes what the enum represents and what the members mean. You do not need to document a constructor. For an Enum with a custom __init__ that attaches extra data to each member, document the __init__ to explain what the extra attributes are and how they relate to the enum value.
from enum import Enum
class SeverityLevel(Enum):
"""Standardized severity ratings for vulnerability findings.
Access by name (SeverityLevel.CRITICAL) or by value (SeverityLevel(9)).
Members map to CVSS base score ranges.
"""
LOW = 1
MEDIUM = 4
HIGH = 7
CRITICAL = 9
class StatusCode(Enum):
"""HTTP-style status codes with human-readable labels attached."""
def __init__(self, numeric_code: int, label: str):
"""Attach a human-readable label to each status code member.
Args:
numeric_code (int): Numeric HTTP status code. Stored as
the member value (self._value_) by the Enum machinery.
label (str): Human-readable description of this status.
"""
self.label = label
OK = 200, "Success"
NOT_FOUND = 404, "Resource not found"
ERROR = 500, "Internal server error"
Protocol classes
typing.Protocol defines structural interfaces — classes that describe what methods and attributes an object must have, without requiring inheritance. They are similar to abstract base classes conceptually, but they are never instantiated directly. You check against them with isinstance(obj, MyProtocol) when using runtime_checkable, or rely on static type checkers like mypy to enforce them.
Because Protocol classes represent contracts rather than constructors, the documentation strategy mirrors abstract base classes: the class docstring describes the interface, and each method carries its own docstring specifying the contract. There is no constructor to document on the Protocol itself. If the protocol has a custom __init__, it typically exists only to store typing metadata — document it only if a developer working with the protocol would need to know about it.
from typing import Protocol, runtime_checkable
@runtime_checkable
class Scannable(Protocol):
"""Structural interface for objects that support network scanning.
Any class with a matching scan() method satisfies this protocol
without inheriting from Scannable. Use for type hints where you
want to accept any scanner regardless of class hierarchy.
"""
def scan(self, timeout: int = 30) -> list:
"""Execute a scan and return findings.
Returns:
list: Scan findings; empty list if nothing found.
"""
Mixed conventions in a real codebase
Teams rarely adopt a new docstring convention on a greenfield codebase. The more common situation is: an existing project that has inconsistent or no docstrings, a team that wants to enforce a standard going forward, and a backlog of legacy classes that do not conform.
The practical approach is a staged migration, not a mass rewrite. Start by locking the convention in pyproject.toml (ruff or pydocstyle) but only enable the rules as warnings, not errors, until new code consistently passes. Existing files that pre-date the rule can be excluded with # noqa: D suppressions at the file level, or via ruff's per-file-ignores configuration. As files are touched for other reasons, update their docstrings then.
The one rule to enforce immediately regardless of migration state: always write the class docstring. An empty or missing class docstring breaks documentation generators silently. Even a single-sentence class docstring — "Represents a network session." — is better than nothing and takes thirty seconds to add. The constructor documentation can wait; the class description cannot.
Attribute docstrings and PEP 258: why Attributes sections exist
The Attributes section in both Google and NumPy style docstrings traces back to a problem that Python's standard docstring mechanism never actually solved. PEP 258 — the companion specification to PEP 257, authored by David Goodger — proposed a formal system for "attribute docstrings": string literals placed immediately after an assignment statement at the class or module level. The idea was that pydoc and documentation tools would recognize these strings as documentation for the preceding attribute.
"Attribute docstrings are not accessible through Python's standard __doc__ mechanism." — PEP 258, David Goodger, peps.python.org
PEP 258 was never implemented in CPython and its status remains "deferred." The Python language has no built-in support for attribute docstrings — a string after an assignment is just a floating expression that gets discarded. This is exactly why Google and NumPy style both introduced manual Attributes sections in their class docstrings: they are working around the gap that PEP 258 would have filled if it had been accepted.
Knowing this history gives the Attributes section a clearer purpose. It is not redundant boilerplate. It is the only standardized way to document public instance variables in a form that documentation generators can reliably find and render, because the language itself offers no automatic mechanism for it. When you write an Attributes section in a class docstring, you are doing manually what PEP 258 would have done automatically.
There is one partial exception: typing.get_type_hints() and tools that read __annotations__ can surface type information for class variables. Sphinx's autodoc extension (with autoattribute) and libraries like attrs and Pydantic have their own first-party documentation systems built around annotations. For standard Python classes, however, the class docstring's Attributes section remains the reliable, toolchain-agnostic answer.
Both Google and NumPy style explicitly exclude @property descriptors from the Attributes section. Properties carry their own docstrings on the getter function itself, so they do not need a separate entry in the class docstring's Attributes section. Only plain instance variables — those set via assignment — belong there.
Enforcing conventions with linting tools
Choosing a convention is only half the work. Without automated enforcement, docstring placement tends to drift — especially in team projects where different contributors default to different habits. Three tools handle this at different levels of strictness.
pydocstyle is the closest to a PEP 257 compliance checker. Running pydocstyle --convention=pep257 yourfile.py flags docstrings that violate the spec, including missing __init__ docstrings on public classes. It also supports --convention=numpy for numpydoc-aware checking and --convention=google for Google style. The project is actively maintained as part of the pydocstyle PyPI package. (pydocstyle.org)
ruff, the fast Python linter written in Rust, includes docstring rules under the D rule set (drawn from pydocstyle). Adding select = ["D"] to your ruff.toml or pyproject.toml activates these checks. Relevant rules for constructor documentation include D107 (missing docstring in __init__) and D205 / D213 (summary line formatting). ruff runs significantly faster than pydocstyle in CI pipelines and is increasingly the standard choice for new projects. (docs.astral.sh/ruff)
numpydoc validation is the right tool when your project follows the numpydoc convention. Running python -m numpydoc --validate yourmodule checks for the GL08 warning code, which flags a missing docstring on a public object. A well-formed class docstring with a Parameters section suppresses this warning for __init__ automatically — which is the behavior the numpydoc convention is designed around. (numpydoc.readthedocs.io/validation)
# pyproject.toml — ruff docstring enforcement example
[tool.ruff.lint]
select = ["D"] # enable all pydocstyle rules
[tool.ruff.lint.pydocstyle]
convention = "google" # or "numpy" or "pep257"
Regardless of which tool you choose, the convention must match. Enabling NumPy-style docstring checking on a Google-style codebase will produce false positives on every class that has an __init__ docstring with an Args section. Lock the convention in pyproject.toml once and let the linter enforce it consistently.
Do type annotations replace the need for docstrings?
This is one of the most searched Python documentation questions, and the answer is: partially, for parameter types — but not for anything else a docstring communicates.
A type annotation tells a reader (and a type checker) what type a parameter accepts. A docstring tells a reader what the parameter means, what constraints apply to it, and what happens at runtime. These are different jobs. ttl: int communicates the type. It does not communicate that the value is in seconds, that zero means "no expiry," or that negative values raise ValueError. The annotation eliminates the need to repeat the type in the docstring — you do not need to write ttl (int): TTL in seconds if the annotation already says int. But the semantic details still belong in the docstring.
The practical rule: when a parameter's name and type annotation are self-explanatory to someone reading the class for the first time, the docstring entry for that parameter adds no value. When there is any ambiguity — units, valid ranges, side effects, sentinel values, mutable defaults — the docstring earns its place regardless of how complete the annotation is.
# Annotation alone is sufficient — no docstring entry needed
def __init__(self, hostname: str, port: int, use_tls: bool = True):
"""Initialize the connection to a remote endpoint."""
# hostname, port, use_tls — names + types say enough
# Annotation is not sufficient — docstring entry adds real information
def __init__(self, timeout: int, retry_on: tuple, backoff: float = 1.5):
"""Configure retry behaviour for a remote call.
Args:
timeout (int): Per-attempt timeout in milliseconds, not seconds.
retry_on (tuple): Exception types that trigger a retry.
Pass an empty tuple to disable retries entirely.
backoff (float): Multiplier applied to timeout on each retry.
A value of 1.0 means no backoff. Defaults to 1.5.
"""
A related question is whether typing.Annotated or third-party validators like Pydantic's Field(description=...) can replace docstrings. For Pydantic models, Field(description=...) is the idiomatic documentation mechanism and feeds directly into the generated JSON schema. For standard Python classes, Annotated metadata is not visible in help() output and is not read by pydoc or Sphinx's standard autodoc — so it does not replace a class docstring for general documentation purposes, though specialized tools can read it.
Key Takeaways
- Always write the class docstring:
ClassName.__doc__is what documentation tools display at the top of your class page. A class with no class-level docstring produces a documentation page with no description, even if__init__is thoroughly documented. - PEP 257 puts constructor args in
__init__: The spec is explicit on this point — constructor parameter documentation belongs in the__init__docstring, not in the class-level docstring. (peps.python.org/pep-0257) The class docstring should summarize behavior and list public methods and instance variables. - Google style uses both, for different things: The class docstring holds
Attributes;__init__holdsArgs. These are not duplicates — they document different aspects of the class. - NumPy / numpydoc style uses the class docstring as the primary location: The numpydoc style guide is explicit — the class docstring's
Parameterssection documents constructor parameters. An__init__docstring is described as optional, used only when additional initialization detail is needed. This is different from "pick one and be consistent" — the class docstring approach is the stated default. (numpydoc.readthedocs.io) - They are separate objects:
MyClass.__doc__andMyClass.__init__.__doc__are independent strings. Filling one does not fill the other. This is the concrete mechanical reason why placement decisions have real consequences in the terminal and in generated documentation. - Type hints reduce docstring pressure: When parameters have descriptive names and accurate type annotations, you often need less text in the docstring. Let annotations carry the type information; use the docstring to explain intent and behavior — especially non-obvious constraints, defaults, and side effects.
- A
Raisessection follows the same placement rule asArgs: Under PEP 257 and Google style, constructor exception documentation belongs in the__init__docstring. Under NumPy style, it belongs in the class docstring'sRaisessection, which appears afterParametersand beforeAttributes. Only document exceptions a caller can anticipate and handle — not every possible internal failure. - Classmethod alternative constructors get their own docstrings: Factory methods like
from_fileorfrom_dictare public methods and should be documented on their owndefline withArgs,Returns, andRaisessections as appropriate. The class docstring can mention they exist, but should not repeat their parameter lists. - Dataclasses have no writable
__init__docstring: The constructor is generated. Document everything in the class docstring. This is not a workaround — it is the correct pattern for the tool. See also: dataclass vs manual __init__ boilerplate. If the class defines__post_init__, that method is handwritten and should have its own docstring, especially if it raises or transforms data. - Subclasses that do not override
__init__inherit the parent's docstring: Write a class docstring on the subclass that describes what is distinct. Do not duplicate the parent's constructor documentation — that creates maintenance risk with no readability benefit. - Classes using
__new__require deliberate documentation: PEP 257 does not address__new__. Treat it as a public method with its own docstring, and use the class docstring to describe the instantiation pattern — especially if the class is a singleton or returns a cached instance.
The class docstring is the front door. It is the first thing a developer sees when they call help() or browse generated documentation. The __init__ docstring is the instruction manual for building an instance — when it exists. PEP 257 treats them as separate responsibilities. Google style does the same, and both conventions also route Raises to the __init__ docstring when the constructor can fail. NumPy / numpydoc style assigns the primary constructor documentation — including Parameters and Raises — to the class docstring's named sections, with __init__ docstrings available as optional supplemental detail. For @classmethod alternative constructors, the rule is consistent across all three: each factory method is a public method and gets its own docstring. And when the class does not follow the standard pattern — a dataclass, a subclass that inherits its constructor, a singleton using __new__ — the underlying principle still applies: document what the code cannot say for itself, put it where tools can find it, and do not repeat what is already visible.
How to choose where your constructor documentation goes
The decision comes down to three questions. Answer them in order and placement becomes mechanical rather than a judgment call.
- Is this a
@dataclass? If yes, there is no hand-written__init__to document. Put everything — including the field descriptions that would otherwise go inArgs— in the class docstring. If the class defines__post_init__, give that method its own docstring. - What convention does your project follow? Check existing class docstrings, your linter configuration, or team style guide. The answer will be one of: PEP 257 / Google style (constructor args go in
__init__) or NumPy / numpydoc style (constructor args go in the class docstring'sParameterssection). - Does your
__init__do anything non-obvious? Even under NumPy style, if the constructor has constrained parameters, units-sensitive defaults, or side effects that the signature cannot express, an__init__docstring adds genuine value. Under PEP 257 and Google style, write one regardless —__init__is a public method and always warrants its own docstring.
Once placement is decided, always write the class docstring first. It is the entry point for help(), Sphinx, and every other documentation tool. The __init__ docstring layers detail on top — it does not replace what belongs at the class level.
PEP 257 / Google style: class docstring for overview and Attributes; __init__ docstring for Args and Raises. NumPy style: class docstring for overview, Parameters, Raises, and Attributes; __init__ docstring is optional. All styles: each @classmethod factory method gets its own docstring.
Frequently asked questions
Does PEP 257 say to put constructor arguments in the class docstring?
No. PEP 257 explicitly routes constructor argument documentation to __init__, not to the class-level docstring. The spec treats __init__ as a public method whose documentation belongs in its own docstring. The class docstring serves a different purpose: summarizing the class's behavior and listing its public methods and instance variables. The two docstrings are complementary, not interchangeable.
Where does NumPy style put constructor parameters?
In the class docstring's Parameters section. This is the default and primary location under the numpydoc convention. An __init__ docstring is explicitly described as optional — warranted only when additional initialization detail is needed beyond what the class docstring already covers. This is a meaningful difference from PEP 257 and Google style, and numpydoc's validation tooling enforces it: a complete class docstring suppresses the GL08 warning that would otherwise flag a missing __init__ docstring.
What does MyClass.__doc__ return vs MyClass.__init__.__doc__?
MyClass.__doc__ returns the string placed immediately after the class definition line. MyClass.__init__.__doc__ returns the string placed immediately after def __init__. They are entirely separate objects. Filling one does not fill the other. If you place all your documentation inside __init__ and leave the class-level string blank, then MyClass.__doc__ returns None and any tool that reads the class description finds nothing.
Does Google style put constructor args in the class docstring?
Not exclusively. The Google Python Style Guide shows a class docstring with an Attributes section, alongside a separate __init__ docstring with an Args section. The two docstrings coexist and serve different purposes — one describes what the class is, the other describes what you pass to build an instance of it.
When should I write an __init__ docstring?
Under PEP 257 and Google style, always — __init__ is a public method and public methods should have docstrings. Under NumPy style, optionally — only when additional initialization detail is needed beyond what the class docstring's Parameters section already covers. Under any convention, a well-named parameter with an accurate type hint sometimes communicates more cleanly than a docstring that merely restates the signature.
Where does the Raises section go in a class docstring vs __init__?
It follows the same placement rule as your parameter documentation. Under PEP 257 and Google style, Raises belongs in the __init__ docstring alongside Args — constructor failure modes are part of constructor behavior. Under NumPy / numpydoc style, Raises belongs in the class docstring's named Raises section, which appears after Parameters and before Attributes. Only document exceptions a caller can realistically anticipate and handle.
How should I document @classmethod alternative constructors like from_dict or from_file?
Each @classmethod constructor is a public method and should carry its own docstring written directly under its def line, with Args, Returns, and Raises sections as needed. The class docstring can note that alternative constructors exist, but should not duplicate their parameter documentation. This approach is consistent across PEP 257, Google style, and NumPy style.
Can I use a linter to enforce where constructor documentation goes?
Yes. pydocstyle supports --convention=pep257, --convention=numpy, and --convention=google modes and flags missing or misplaced docstrings. ruff provides the same checks via its D rule set (set convention = "google" or "numpy" in [tool.ruff.lint.pydocstyle] in your pyproject.toml). For numpydoc projects, python -m numpydoc --validate checks for the GL08 warning code, which flags missing docstrings on public objects. The important constraint: the linter's convention setting must match your project's convention, or you will get false positives on correctly written docstrings.
How does Sphinx handle the __init__ docstring by default?
By default, Sphinx's autoclass directive displays only the class-level docstring — ClassName.__doc__. The __init__ docstring is not shown unless you explicitly configure it. There are two ways to include it: pass :special-members: __init__ on the autoclass directive, or set autoclass_content = 'both' in your conf.py, which appends the __init__ docstring to the class description for all classes globally. A third option, autoclass_content = 'init', shows only the __init__ docstring and suppresses the class-level one — almost never the right choice, since it hides the class description from the generated documentation page entirely.
Do type annotations replace the need for parameter documentation in a docstring?
For the type itself, yes — if a parameter is annotated timeout: int, you do not need to write timeout (int): in the docstring. The annotation already communicates the type to both humans and tools. What annotations cannot communicate is semantics: that the value is in milliseconds not seconds, that zero disables the timeout entirely, that negative values raise ValueError, or that a mutable default has a specific initialization behavior. Those details still belong in the docstring. The practical test: would a developer using your class for the first time be able to call the constructor correctly with only the annotations visible? If yes, a minimal or absent Args section is defensible. If the annotation leaves any ambiguity, write the docstring entry.
How should I document a class that uses __slots__?
Classes that define __slots__ follow the same docstring conventions as any other class — the choice of __slots__ is a memory optimization detail that does not change where documentation goes. The class docstring describes what the class represents. If you follow Google or NumPy style, the Attributes section should list the same names that appear in __slots__ along with their purpose. The __init__ docstring documents the constructor parameters as usual. One thing worth noting in the class docstring: that the class uses __slots__, so callers know that dynamic attribute assignment will raise AttributeError — this is a behavioral detail that the slot declaration alone does not surface in documentation output.