← Back to Home

Python Enum Guide

Python · Enum, IntEnum, StrEnum

Complete Python Enum Guide

Python 3.4 introduced the Enum module, bringing type-safe enum support to a dynamic language. From basic Enum classes to specialized IntEnum and StrEnum, to integration with modern frameworks like Pydantic and FastAPI, this guide will help you use enums correctly in Python projects.

This guide covers enum definitions, auto-assignment, type hints, string conversion, database mapping, and web framework integration.

Python Enum Fundamentals

Python's Enum module provides multiple enum types: basic Enum, IntEnum which can be compared with integers, and StrEnum introduced in Python 3.11. The choice depends on your use case—whether you need serialization to specific types, compatibility with legacy code, etc.

Core API Quick Reference

Definition

from enum import Enum, auto

class OrderStatus(Enum):
    PENDING = 'pending'
    PAID = 'paid'
    REFUNDED = auto()

Access

OrderStatus.PAID.value  # 'paid'
OrderStatus['REFUNDED']  # 
list(OrderStatus)        # Enum iteration
  • ✔ **Enum vs IntEnum vs StrEnum**: Basic Enum is the strictest, IntEnum can participate in math operations, StrEnum serializes directly to strings
  • ✔ **auto() auto-assignment**: Let Python automatically assign incremental values, reducing manual maintenance
  • ✔ **unique decorator**: Ensures enum values are not duplicated, detecting issues at definition time

Practical Patterns & Best Practices

This section covers how to combine type hints, auto-assignment, and string conversion to write robust production-level code.

Type Hints & Data Validation

Python's type system combined with enums can provide compile-time checks. Pydantic and FastAPI can automatically recognize enum types and display available values in API documentation.

from enum import StrEnum
from pydantic import BaseModel, Field

class Role(StrEnum):
    ADMIN = 'admin'
    EDITOR = 'editor'
    VIEWER = 'viewer'

class User(BaseModel):
    username: str
    role: Role = Field(default=Role.VIEWER)

# FastAPI endpoint automatically generates enum schema
  • ✔ FastAPI automatically displays all enum values in Swagger UI
  • ✔ Add descriptions via Field to generate more friendly API documentation
  • ✔ StrEnum was introduced in Python 3.11, older versions need custom implementation or use Enum

Enum & Database Mapping

When storing enum values in databases, you need to establish mapping relationships between application-level enums and database fields.

  • SQLAlchemy – Use `Enum(Role, name="role")` to create database-level enum types
  • PostgreSQL – Supports native ENUM types, requires migration scripts to create
  • MySQL – Can use native ENUM fields, but modifications are less flexible

Each approach has trade-offs, consider modification frequency and cross-database compatibility when choosing.

Framework Ecosystem: Django, FastAPI, Pydantic

In Python backend ecosystems, enums are frequently used to define API parameters, response statuses, etc. Let's see how to use enums in mainstream frameworks.

Pydantic / FastAPI

FastAPI natively supports Python enums and automatically displays all available values in Swagger documentation, eliminating the need for manual documentation.

Enum fields in Pydantic models automatically validate input values, rejecting invalid values directly.

Django & Django REST Framework

Django 3.0 introduced TextChoices and IntegerChoices, providing a more convenient way to define enums. They can be used directly in model fields and forms.

Django REST Framework serializers automatically handle serialization and deserialization of these enum types.

Python Enum Playbook

Python Enum Cheat Sheet

  • • Complete examples: Enum, IntEnum, StrEnum, auto(), unique, Literal, Pydantic
  • • JSON Schema templates: adapted for FastAPI, Pydantic, GraphQL
  • • Naming conventions: PEP 8 style and team agreements

Team Code Review Checklist

✓ Type Selection

  • • Prefer `StrEnum` (Python 3.11+) for string enums
  • • Use `IntEnum` when integer mapping is needed, avoid plain `Enum` + integer values
  • • Use `@unique` decorator to prevent duplicate values
  • • Consider using `typing.Literal` instead of simple string enums

✓ Serialization

  • • Enums in FastAPI/Pydantic models are automatically validated and documented
  • • Use `member.value` for database storage, don't store `member.name`
  • • Explicitly specify `.value` attribute for JSON serialization
  • • Use `__str__` and `__repr__` to customize display format

✓ Compatibility

  • • Evaluate impact on existing API contracts when adding enum members
  • • Avoid deleting or renaming published enum values
  • • Document enum change history in migration docs
  • • Use type annotations to ensure mypy/pyright checks pass

Naming and Design Anti-patterns

❌ Anti-pattern: Using string constants instead of enums

STATUS_PENDING = "pending"
STATUS_COMPLETED = "completed"

def process(status: str):  # No type constraints!
    if status == STATUS_PENDING:
        ...

Issues: typos, no IDE autocomplete, cannot enumerate all possible values

✅ Correct: Use StrEnum

from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    COMPLETED = "completed"

def process(status: Status):  # Type safe!
    if status == Status.PENDING:
        ...

❌ Anti-pattern: Mixing .name and .value

class Status(Enum):
    PENDING = 1
    COMPLETED = 2

# Database stored "PENDING", but should store 1
db.save(status.name)  # Dangerous!

✅ Correct: Always use .value

class Status(IntEnum):
    PENDING = 1
    COMPLETED = 2

db.save(status.value)  # Always store numeric value

❌ Anti-pattern: Allowing duplicate values

class Status(Enum):
    PENDING = 1
    PROCESSING = 1  # Accidental duplicate!
    COMPLETED = 2

✅ Correct: Use @unique decorator

from enum import Enum, unique

@unique
class Status(Enum):
    PENDING = 1
    PROCESSING = 1  # Will error at definition time!
    COMPLETED = 2

Pydantic + FastAPI Complete Example

Standard enum usage in modern Python APIs:

from enum import StrEnum
from pydantic import BaseModel
from fastapi import FastAPI

class OrderStatus(StrEnum):
    """Order status enum"""
    PENDING = "pending"
    PROCESSING = "processing"
    COMPLETED = "completed"
    FAILED = "failed"

class Order(BaseModel):
    id: int
    status: OrderStatus  # Auto-validation and documentation
    
    class Config:
        use_enum_values = True  # Use string values in JSON

app = FastAPI()

@app.post("/orders")
def create_order(status: OrderStatus):
    # status parameter appears as dropdown in Swagger
    return {"status": status.value}

Tip: FastAPI automatically displays all enum options in Swagger UI

Enum Consistency Test Script

Ensure enums remain consistent across layers:

import pytest
from your_app.models import OrderStatus

def test_enum_all_values_are_strings():
    """Ensure all enum values are strings"""
    for member in OrderStatus:
        assert isinstance(member.value, str)

def test_enum_no_duplicates():
    """Ensure no duplicate values"""
    values = [m.value for m in OrderStatus]
    assert len(values) == len(set(values))

def test_enum_serialization_roundtrip():
    """Test serialization roundtrip"""
    for member in OrderStatus:
        assert OrderStatus(member.value) == member

def test_enum_matches_frontend_contract():
    """Ensure values match frontend contract"""
    expected = {"pending", "processing", "completed", "failed"}
    actual = {m.value for m in OrderStatus}
    assert actual == expected

@pytest.mark.parametrize("status", list(OrderStatus))
def test_all_statuses_handled(status):
    """Ensure business logic handles all statuses"""
    result = process_order_status(status)
    assert result is not None

Tip: Using parametrize ensures tests automatically cover new enum values

Frontend-Backend Enum Sync Strategy

Option 1: Generate TypeScript from OpenAPI

# Export OpenAPI schema
uvicorn main:app --host 0.0.0.0 --port 8000

# Use openapi-typescript to generate types
npx openapi-typescript http://localhost:8000/openapi.json -o schema.ts

Option 2: Manually maintain TypeScript enums

// frontend/types/order.ts
export const OrderStatus = {
  PENDING: "pending",
  PROCESSING: "processing",
  COMPLETED: "completed",
  FAILED: "failed",
} as const;

export type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];

Option 3: Use Zod validation

import { z } from "zod";

export const OrderStatusSchema = z.enum([
  "pending", "processing", "completed", "failed"
]);

export type OrderStatus = z.infer<typeof OrderStatusSchema>;

Recommendation: Option 1 is most automated, Option 3 provides runtime validation

Python Enum FAQ

python enum vs class constant?

Using enums provides value constraints, member iteration, type hints, and clearer semantics; class constants are lighter but lack these features.

How does python enum auto ensure stability?

`auto()` automatically increments. For persistence needs (database, serialization), explicitly specify `value` or use `StrEnum` to ensure version compatibility.

How to use Python enums in JSON and frontend?

Typically return `member.value` (prefer strings), ensure TypeScript/JavaScript uses `as const` or Zod enum for type validation.

How to cover all enum values in tests?

Use `for member in EnumClass` to iterate, combined with `pytest.mark.parametrize` or snapshot tests to ensure developers are reminded to handle new enum values.