Refactor Dataclasses, Null Safety, and Typestate¶
Page Maps¶
graph LR
family["Python Programming"]
program["Python Object-Oriented Programming"]
section["State Validation Typestate"]
page["Refactor Dataclasses, Null Safety, and Typestate"]
capstone["Capstone evidence"]
family --> program --> section --> page
page -.applies in.-> capstone
flowchart LR
input["separate raw input from domain state"] --> validity["make objects begin life honest"]
validity --> absence["repair nulls and partial objects"]
absence --> lifecycle["name the lifecycle and transitions"]
lifecycle --> API["narrow the API around legal operations"]
This refactor matters because Module 03 can still collapse into feature trivia if the learner leaves with only isolated ideas:
- use
@dataclass - add
__post_init__ - avoid too many
Optionals - maybe use properties
The real goal is stronger state honesty. This page exists to turn the module into one repeatable repair route for weak state models.
The weak starting shape¶
A weak pre-refactor design usually looks like this:
- raw input becomes domain state too early
- objects are created before they are really valid
Nonespreads because lifecycle is blurry- dataclasses are used for convenience without semantic review
- every method stays callable regardless of state
The code may run, but another maintainer cannot tell when the object is legal or which operations should disappear after a transition.
The stronger target shape¶
The target is not "more defensive code." The target is a model that tells the truth:
- boundary input is validated before core object creation
- objects begin life in an honest state
- absence is modeled deliberately instead of by habit
- lifecycle stages are named and enforced
- dataclasses are used only where their generated behavior matches the contract
- the API surface changes with the lifecycle instead of pretending every method always applies
Use this refactor order¶
Follow this order on purpose:
- separate boundary parsing from domain construction
- identify the real invariants of each core object
- remove fake optionality from required fields
- distinguish true absence from earlier lifecycle state
- name the lifecycle explicitly
- tighten the API around legal transitions
- only then keep or add supporting machinery that genuinely helps
This order matters because later improvements depend on earlier clarity.
Step 1: stop importing uncertainty into the model¶
Start by separating:
- raw payloads
- parsing and normalization
- domain construction
A common smell is a domain object that accepts:
- strings that still need parsing
- timestamps that might be absent for several different reasons
- caller guesses about state
That uncertainty belongs at the boundary first. The domain model should receive sharper inputs than the transport surface does.
Step 2: make the constructor honest¶
Ask:
- what must be true the moment this object exists?
- which fields are truly required?
- which current constructor arguments are placeholders for a later transition?
Good result:
- the constructor creates a small honest state
- later transitions are named methods instead of optional fields passed "just in case"
This is often the point where the design first becomes easier to trust.
Step 3: repair overloaded absence¶
One nullable field often hides several meanings:
- not submitted yet
- not loaded yet
- rejected and reopened
- broken imported state
A stronger refactor splits those meanings by:
- a state value
- clearer timestamps
- a dedicated value object
- smaller explicit transitions
The goal is not fewer Nones for style reasons. The goal is fewer lies about meaning.
Step 4: review the dataclass honestly¶
Once state is sharper, decide whether @dataclass still helps.
Ask:
- does generated equality match the contract?
- are defaults honest or only convenient?
- is mutability still safe?
- do
frozen,slots, or post-init checks clarify or distort the design?
The answer may be:
- keep the dataclass
- narrow the dataclass
- replace it with a more explicit class
There is no prize for keeping @dataclass if it weakens the model.
Step 5: name the lifecycle and transitions¶
The refactor should leave behind a lifecycle table.
| State | What is true | Allowed operations | Illegal operations |
|---|---|---|---|
If you cannot fill that table, the lifecycle is still too blurry.
Once the states are named, decide:
- which transitions are allowed
- which transitions are forbidden
- which boundary enforces each move
This is where the object stops being a bag of fields and becomes a stateful contract.
Step 6: narrow the public API¶
A stronger state model should make misuse less natural.
Questions to ask:
- which methods should exist only in draft-like states?
- which methods belong only after submission or activation?
- which surfaces should become read-only after approval, retirement, or finalization?
Even without static type tricks, runtime state splits and smaller method sets can make illegal operations feel obviously wrong.
A worked refactor route¶
Imagine a ReportDraft-like design with:
title: str | Nonesubmitted_at: datetime | Noneapproved_at: datetime | Nonestatus: str- methods callable in every state
A safer Module 03 refactor route is:
- keep raw request parsing outside the domain object
- make the constructor create only honest drafts
- replace one overloaded nullable field with explicit state and timestamps
- move submission, approval, rejection, and reopening into named transition methods
- narrow the API so edit methods disappear from final states
- review whether the dataclass still helps after the lifecycle is explicit
The result is not merely "cleaner code." It is a clearer answer to when the object is legal.
How to review the result¶
After refactoring, ask:
- can this object exist in this shape?
- what state is it in right now?
- where is invalid input rejected first?
- is this field absent by meaning or by design weakness?
- which operations are no longer legal after the transition?
If those answers are still fuzzy, the refactor is not finished.
Capstone transfer¶
Use the capstone immediately:
- where do policy and rule objects begin life valid?
- which transitions are legal for registration, activation, retirement, or incident state?
- where is absence meaningful instead of accidental?
- which APIs should narrow after lifecycle changes?
This page should make the capstone easier to audit, not just easier to describe.
Exit check¶
Leave this lesson only when you can do all of these:
- describe the target Module 03 design shape in your own words
- explain an order for refactoring a weak state model into a stronger one
- identify one capstone area that would become clearer if you applied this route