What does annotating a dataclass attribute with dataclasses.InitVar do?
answer
- In the constructor, not on the instance
- A pseudo-field, never a real field
- Forwarded to the construction hook
- Absent from fields(), repr and equality
- Compare it with ClassVar
basics
~20 sdataclasses.InitVar marks a construction-only pseudo-field. It becomes a parameter of the generated init and is handed to post_init, but it is never stored on the instance and never appears in fields(), the repr, equality or asdict().
solid answer
~40 sAnnotating a name as `InitVar[T]` tells `@dataclass` to treat it as a pseudo-field: the decorator adds it to the generated `__init__` signature, in declaration order, alongside the real fields, but creates no attribute for it and no entry in `dataclasses.fields()`. Instead the constructor forwards its value to `__post_init__` as a positional argument, so the hook can use it and then discard it. That is the point — it carries information needed only to *build* the object, such as a raw string to parse, a seed, or a flag that selects how a derived field is computed, without becoming part of the value's identity in `__repr__`, `__eq__` or `dataclasses.asdict()`. An `InitVar` may have a default, and it obeys the same ordering rule as any other constructor parameter: no non-defaulted name may follow a defaulted one.
code
python · 14 linesfrom dataclasses import InitVar, dataclass, field, fields
@dataclass
class ConversionJob:
doc_id: str
raw_pages: InitVar[str]
pages: tuple[int, ...] = field(init=False)
def __post_init__(self, raw_pages: str) -> None:
self.pages = tuple(int(p) for p in raw_pages.split(","))
job = ConversionJob("doc-27", "1,2,3")
print(job)
print([f.name for f in fields(job)])go deeper
Recognize the marker and what it buys: a value the constructor accepts purely to build the object, handed to post_init and then dropped rather than stored as an attribute.
Explain the mechanics precisely: it is a pseudo-field, it keeps its slot in the init signature and its ordering rules, it is passed positionally to post_init, and it is invisible to fields(), repr, eq and asdict().
Show the design judgement: use it for raw input, build flags and short-lived collaborators, and know the downstream cost — replace() must be given the value again, while copy and pickle bypass the constructor entirely.
Weigh whether construction-time arguments belong in the value type at all, or in a factory that produces a clean record; an InitVar quietly widens the constructor's contract that every caller and every serializer round-trip must honour.
## Pseudo-field, not field A dataclass field is an annotated class-level name that the decorator turns into a constructor parameter *and* an instance attribute. `dataclasses.InitVar[T]` breaks that pairing: the name still becomes a constructor parameter, but no attribute is ever assigned for it. `@dataclass` detects the annotation while it walks the class, records the name as a pseudo-field, and treats it specially everywhere else — it is absent from `dataclasses.fields()`, from the generated `__repr__` and `__eq__`, and from `dataclasses.asdict()`. The only place its value surfaces is `__post_init__`, which receives one positional parameter for each `InitVar`, in the order the annotations appear. ```python from dataclasses import InitVar, dataclass, field, fields @dataclass class Job: doc_id: str raw_pages: InitVar[str] pages: tuple[int, ...] = field(init=False) def __post_init__(self, raw_pages: str) -> None: self.pages = tuple(int(p) for p in raw_pages.split(",")) ``` `Job("doc-27", "1,2,3")` reprs as `Job(doc_id='doc-27', pages=(1, 2, 3))`, and `[f.name for f in fields(job)]` is `['doc_id', 'pages']`. The raw string was an input, not part of the value. ## What it is for Three recurring shapes: 1. **Raw input that is parsed into a real field.** The caller has a string, a path or a blob; the object wants the parsed form. The `InitVar` carries the raw thing in, `__post_init__` parses it into a `field(init=False)`, and the unparsed version never pollutes equality or serialization. 2. **A construction-time flag.** `strict: InitVar[bool] = False` changes how the object validates or derives itself, without becoming state that later code can read and misinterpret. Two objects built with different flags but identical resulting values compare equal — usually exactly what you want. 3. **A collaborator needed only to build.** A clock, a loader or a lookup table passed in so `__post_init__` can resolve something, then dropped instead of being pinned into the instance where the repr would print it and equality would compare it. If you find yourself storing the `InitVar` value on `self` anyway, you did not want an `InitVar`; you wanted an ordinary field. ## Ordering, defaults and typing An `InitVar` occupies a position in the constructor signature exactly where it is declared, so it participates in the usual rule that a parameter without a default cannot follow one with a default. It may carry a default of its own (`verbose: InitVar[bool] = False`), and that default is what `__post_init__` receives when the caller omits it. The subscript is documentation for readers and type checkers — `InitVar[str]` says the parameter is a `str` — and it is not enforced at runtime. The matching parameter in `__post_init__` should be annotated with the *inner* type, `str`, not with `InitVar[str]`. Detection is by annotation, and it is shallower than it looks: the decorator recognizes the `InitVar` marker on the annotation, and when annotations are strings it falls back to matching the written text. Rebinding it under another name and annotating with the alias can therefore leave the decorator treating the name as an ordinary field, which shows up as a surprise attribute and an extra entry in the repr. Write `InitVar[...]` literally. ## Consequences downstream Because an `InitVar` is a constructor parameter with no stored value, anything that rebuilds an instance through `__init__` has to be told what to pass. `dataclasses.replace()` cannot reconstruct one from the existing object — there is nothing to read — so an `InitVar` without a default must be supplied in the call, and omitting it raises `TypeError: InitVar 'raw_pages' must be specified with replace()`. In the other direction, `copy.deepcopy` and `pickle` restore instance state directly instead of calling the constructor, so they never need the `InitVar` at all, and never re-run the parsing that consumed it. Also note the ordinary `typing.ClassVar` annotation for contrast: `ClassVar` excludes a name from the fields *and* from the constructor, marking it as shared class state; `InitVar` excludes it from the fields but keeps it in the constructor. They are the two ways an annotated name in a dataclass body can fail to become a field, and interviewers like the pairing. ## Several InitVars, and inheritance The `__post_init__` signature must match the pseudo-fields exactly: one parameter per `InitVar`, in declaration order, and no more. Get it wrong and the failure is at construction time, not class-creation time — `TypeError: Job.__post_init__() takes 1 positional argument but 2 were given`, raised from inside the generated constructor. That is a mismatch worth checking whenever you add an `InitVar` to a class that already has the hook. Inheritance compounds it. Pseudo-fields are inherited like fields, and the constructor forwards *all* of them, base-declared ones included, to whichever `__post_init__` attribute lookup finds. A subclass that overrides the hook must therefore accept the base's `InitVar` parameters even if it does nothing with them, and pass them along if it calls `super().__post_init__(...)`. This is the practical argument for keeping `InitVar` use shallow: in a hierarchy the constructor-only arguments accumulate into every override's signature.
- How does dataclasses.replace() behave on a class that declares an InitVar without a default?It raises `TypeError: InitVar 'x' must be specified with replace()`. `replace()` rebuilds the object by calling `__init__` with the existing field values merged with your changes, and an `InitVar` has no stored value to read back, so it must be supplied explicitly in the call. An `InitVar` with a default is fine, because the default is used.
- What is the difference between annotating a name InitVar[int] and typing.ClassVar[int] inside a dataclass body?Both stop the name becoming a field. `ClassVar` removes it from the constructor as well and leaves it as ordinary shared class state, untouched by the decorator. `InitVar` keeps it as a constructor parameter and forwards its value to `__post_init__`, but stores nothing on the instance. So `ClassVar` is class-level data; `InitVar` is a construction-only argument.
- Should the corresponding __post_init__ parameter be annotated InitVar[str] or str?`str` — the inner type. `InitVar` is a marker for the decorator's field-collection pass over the class body; by the time the value reaches `__post_init__` it is just a value of the wrapped type. Annotating the parameter as `InitVar[str]` is not an error at runtime but misleads readers and type checkers about what the method actually receives.
saying these in an interview costs you the question
- Thinks an InitVar becomes an instance attribute
- Expects it in fields(), the repr or asdict()
- Says it is only a type-checker hint with no runtime effect
- Confuses it with ClassVar, which leaves the constructor
- Believes InitVar parameters can be reordered freely
- Assumes InitVar[str] validates the argument at runtime