We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Stdlib Mastery step 47 of 55
StrEnum: migrate a module of string constants without breaking callers
Replace a module of bare string constants with a StrEnum, and prove that
every existing use site still works.
@unique
class Status(StrEnum):
PENDING = "pending"
ACTIVE = "active"
DONE = "done"
FAILED = "failed"
class Legacy(StrEnum):
OLD = "same"
NEW = "same" # deliberately an alias -- no @unique here
PRIORITY: dict[str, int] = {"pending": 0, "active": 1, "done": 2, "failed": 3}
def parse_status(raw: str) -> Status: ...
def matches_raw(status: Status, raw: str) -> bool: ...
def solve(raw: list[str]) -> dict[str, object]: ...
solve returns:
| key | value |
|---|---|
"values" |
[str(m) for m in Status] — declaration order |
"json" |
json.dumps({"status": Status.ACTIVE, "history": [Status.PENDING, Status.ACTIVE]}) |
"eq_raw" |
matches_raw(Status.PENDING, "pending") |
"formatted" |
f"{Status.PENDING}" |
"lookup" |
PRIORITY[Status.DONE] — indexing a dict[str, int] with the member |
"parsed" |
for each input, parse_status(text).name or "ValueError" |
"duplicates" |
"ValueError" if unique(Legacy) raises, else "accepted" |
"aliased" |
Legacy.NEW.name |
Why StrEnum and not a plain Enum. A StrEnum member is a str, so
every existing comparison, dict lookup, log line and json.dumps keeps
working unchanged — which is what makes the migration affordable. A plain
Enum with the same members gives 'Status.PENDING' from str(), False
from == "pending", a KeyError on the dict lookup, and a TypeError from
json.dumps. Migrating to that means touching every call site.
What Legacy demonstrates. Without @unique, a repeated value creates an
alias: Legacy.NEW is Legacy.OLD, Legacy.NEW.name is "OLD", and
iteration yields one member. Occasionally that is what you want for a
deprecated spelling; far more often it is a copy-paste bug whose symptom is a
log line reporting a name nobody wrote. unique() turns it into a
ValueError at class-creation time.
A --strict detail you will hit. Status.PENDING == "pending" written
literally is a comparison-overlap error: with two literal operands mypy
decides the comparison is non-overlapping. Route it through a function taking
raw: str, which is also where such a comparison belongs in real code — at
the boundary where an untyped string arrives.
Verified checker split, worth knowing. mypy rejects a StrEnum member
where a matching Literal is expected; pyright and ty accept it, and mypy
issue #19243 was closed as not planned. So “StrEnum or Literal?” is partly a
question of which checker your team runs — do not try to mix them at an API
boundary.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.