Python StrEnum: String-Based Enums
python strenum: Learn how Python 3.11's StrEnum creates string-based enums, simplifies serialization, and replaces the str, Enum mixin pattern.
Python 3.11 added StrEnum to the standard enum module. The python strenum feature gives developers a way to define enum members that are also real string instances, usable directly in string contexts without calling .value. A StrEnum member is both an enum member and a string, which removes the need for explicit conversion in most real-world code.
from enum import StrEnum class Status(StrEnum): PENDING = "pending" ACTIVE = "active" BLOCKED = "blocked"
Status.PENDING is a Status member, but it also satisfies isinstance(Status.PENDING, str). String methods work directly: Status.ACTIVE.upper() returns "ACTIVE", and comparisons against plain strings succeed.
Basic StrEnum Declaration
Declaring a StrEnum is similar to declaring a regular Enum, with one constraint: every value must be a string. Attempting to assign a non-string value raises TypeError at class creation time.
from enum import StrEnum class Environment(StrEnum): DEV = "development" STAGING = "staging" PROD = "production"
The value is what gets stored in databases, sent over the wire, or written to logs. Because the member itself is a string, you can pass Environment.PROD to a function that expects a str parameter without calling .value.
How StrEnum Differs from a str, Enum Mixin
Before Python 3.11, the common way to create string-valued enums was to mix str with Enum:
from enum import Enum class Status(str, Enum): PENDING = "pending" ACTIVE = "active"
This works for basic cases: members are str instances, and str(Status.PENDING) returns "pending". But the mixin approach has two practical gaps. First, it does not validate that values are strings; class Status(str, Enum): CODE = 200 is accepted even though the member cannot behave as a meaningful string. Second, auto() generates integer values, so class Status(str, Enum): PENDING = auto() produces a member whose value is 1, not "PENDING".
StrEnum closes both gaps. Values must be strings, and auto() uses the member name as the value.
| Behavior | class Status(str, Enum) | StrEnum |
|---|---|---|
Member is a str instance | Yes | Yes |
str(Status.PENDING) | "pending" | "pending" |
auto() value | Integer (1, 2, ...) | Member name ("PENDING") |
| Non-string value allowed | Yes | No (TypeError) |
| Available since | Python 3.4 | Python 3.11 |
Using StrEnum Members as Strings
Because StrEnum members are real strings, they integrate cleanly with string-based APIs. This is the main practical advantage.
import json from enum import StrEnum class OrderStatus(StrEnum): NEW = "new" SHIPPED = "shipped" DELIVERED = "delivered" order = {"id": 42, "status": OrderStatus.SHIPPED} payload = json.dumps(order)
json.dumps serializes OrderStatus.SHIPPED directly because it is a str subclass. No custom encoder is required. The same applies to f-strings, logging calls, and any function annotated with str.
Generating Values with auto()
StrEnum supports auto(), which generates the value from the member name. The generated value is the member name itself.
from enum import StrEnum, auto class Permission(StrEnum): READ = auto() WRITE = auto() EXECUTE = auto()
Permission.READ equals "READ", Permission.WRITE equals "WRITE", and so on. This is useful when the external representation should match the internal name and you want to avoid repeating the string literal. For values that differ from the member name, assign them explicitly.
Comparing StrEnum with IntEnum
IntEnum has been part of Python since 3.4 and behaves similarly to StrEnum, but for integers. The choice between them depends on the data you are modeling.
| Aspect | IntEnum | StrEnum |
|---|---|---|
| Member type | int | str |
auto() values | Sequential integers starting at 1 | Member name as string |
| JSON serialization | Needs custom handling for some cases | Works directly |
| Typical use | Numeric codes, flags, bitmasks | API values, config keys, labels |
Use StrEnum when the values are human-readable and will appear in logs, responses, or configuration files. Use IntEnum when the values are numeric codes from an external system or when integer comparison semantics matter.
Serialization and API Usage
One of the most common places StrEnum pays off is in API layers. When a request handler returns a status field, the StrEnum member can be returned directly and serialized without a conversion step.
from enum import StrEnum class PaymentState(StrEnum): PENDING = "pending" COMPLETED = "completed" FAILED = "failed" def payment_response(payment_id: int, state: PaymentState) -> dict: return {"payment_id": payment_id, "state": state}
The response dictionary contains a real string, so any JSON encoder handles it. On the input side, you can validate incoming strings by constructing the enum: PaymentState(request_json["state"]) raises ValueError for unknown values, which gives you validation for free.
Edge Cases and Compatibility
StrEnum requires Python 3.11 or later. If your codebase must support earlier versions, the str, Enum mixin remains the compatible alternative, with the auto() and validation differences noted earlier.
One edge case to watch is case sensitivity. Status("PENDING") raises ValueError if the enum defines "pending" with lowercase. If you need case-insensitive lookup, normalize input before constructing the enum.
Another limitation: StrEnum members cannot have non-string values. Attempting to define class X(StrEnum): A = 1 raises a TypeError at class creation. If you need mixed-type values, a regular Enum is the appropriate choice.