Back to Blog
Python

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.

StrEnumPython enumPython 3.11string serializationtype safety
Diagram showing Python StrEnum members connected to their string values, illustrating that enum members behave as strings

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.

Behaviorclass Status(str, Enum)StrEnum
Member is a str instanceYesYes
str(Status.PENDING)"pending""pending"
auto() valueInteger (1, 2, ...)Member name ("PENDING")
Non-string value allowedYesNo (TypeError)
Available sincePython 3.4Python 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.

AspectIntEnumStrEnum
Member typeintstr
auto() valuesSequential integers starting at 1Member name as string
JSON serializationNeeds custom handling for some casesWorks directly
Typical useNumeric codes, flags, bitmasksAPI 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.

python strenum: Practical Usage and Code Examples | RYUSLOG DEV