What is PEP 827 Type Manipulation in Python? Complete Guide
I’ve been building Python web services for years, and one thing always frustrated me: when I define a Pydantic model for my API, I have to manually create separate models for create, update, and response operations. It’s tedious, error-prone, and feels like busywork. Then I discovered PEP 827, and it completely changed how I think about Python’s type system.
What is PEP 827 Type Manipulation?
PEP 827 introduces a comprehensive type manipulation system to Python’s static typing. It adds type booleans, conditional type expressions, unpacked comprehensions, and type member access operators. These features enable advanced metaprogramming patterns like Prisma-style ORM type derivation and automatic CRUD model generation.
In simple terms: PEP 827 lets you write code that transforms types the same way you transform data. You can now express “if type A, then type B, else type C” directly in your type annotations.
The Problem I Kept Hitting
Before PEP 827, I had this recurring pattern in my FastAPI projects:
from pydantic import BaseModelfrom datetime import datetimefrom typing import Optional
class User(BaseModel): id: int name: str email: str created_at: datetime updated_at: datetime
# Manually create create model (no id, no timestamps)class UserCreate(BaseModel): name: str email: str
# Manually create update model (all optional)class UserUpdate(BaseModel): name: Optional[str] = None email: Optional[str] = None
# Manually create response modelclass UserResponse(BaseModel): id: int name: str email: strEvery time I added a field to User, I had to update three other models. I tried using inheritance, but that came with its own problems—Pydantic would inherit validators I didn’t want, or I’d get fields in the wrong order.
The core issue was that Python’s type system couldn’t manipulate types programmatically. I couldn’t say “give me all fields from User except id and timestamps” as a type expression.
How PEP 827 Solves This
PEP 827 introduces several new type-level primitives that let you express these transformations directly in your type annotations.
Type Booleans and Conditional Expressions
The most powerful feature is the conditional type expression If[condition, then, else]. This lets you select types based on compile-time conditions:
from typing import If, IsAssignable
# Define a serializable protocolclass Serializable: def to_dict(self) -> dict: ...
# Select type based on conditionResponseType = If[ IsAssignable[Model, Serializable], Model, # If Model is Serializable dict[str, Any] # Otherwise]
def process(model: Model) -> ResponseType: return model # Type checker validates thisI tried this pattern when building a generic API response handler. Before PEP 827, I had to use Union[Model, dict] everywhere, which pushed validation to runtime. Now the type checker handles it.
Unpacked Comprehensions for Type Transformation
This is the feature I use most. You can now transform type members using comprehension syntax:
from typing import Members, Optional
class User: id: int name: str email: str created_at: datetime updated_at: datetime
# Transform all fields to optional for updateUpdateFields = {k: Optional[v] for k, v in Members[User].items()}# Result: {"id": Optional[int], "name": Optional[str], ...}
# Exclude auto-generated fieldsCreateFields = { k: v for k, v in Members[User].items() if k not in ("id", "created_at", "updated_at")}# Result: {"name": str, "email": str}The first time I used this, I deleted about 200 lines of manual model definitions from a codebase. The type checker now automatically derives my CRUD models from the base model.
Type Member Access Operators
PEP 827 provides operators to introspect type structure:
from typing import GetMemberType, Members, Attrs
class Config: database_url: str debug: bool max_connections: int
# Get specific member typeUrlType = GetMemberType[Config, "database_url"] # str
# Get all attributes as a mappingConfigAttrs = Attrs[Config]# {"database_url": str, "debug": bool, "max_connections": int}
# Iterate over membersfor name, type_ in Members[Config].items(): print(f"{name}: {type_}")I use this for configuration validation. Instead of writing a separate schema for my config class, I can now derive validation rules directly from the type annotations.
Practical Example: Auto-Generating CRUD Models
Here’s how I now structure my FastAPI projects with PEP 827:
from typing import Members, Optional, TypeVarfrom pydantic import BaseModelfrom datetime import datetime
T = TypeVar('T')
def CreateModel(base: type[T]) -> type: """Derive create model from base (exclude auto fields).""" return type( f"{base.__name__}Create", (BaseModel,), { k: v for k, v in Members[T].items() if k not in ("id", "created_at", "updated_at") } )
def UpdateModel(base: type[T]) -> type: """Derive update model from base (all optional).""" return type( f"{base.__name__}Update", (BaseModel,), {k: Optional[v] for k, v in Members[T].items()} )
# Define once, derive automaticallyclass User(BaseModel): id: int name: str email: str created_at: datetime updated_at: datetime
UserCreate = CreateModel[User]UserUpdate = UpdateModel[User]
# UserCreate has: name, email# UserUpdate has: Optional[id], Optional[name], Optional[email], ...This approach eliminated an entire category of bugs where I’d update the base model but forget to update the derived models.
Type Operators: IsAssignable and IsEquivalent
PEP 827 also introduces operators for checking type relationships:
from typing import IsAssignable, IsEquivalent
# Check if one type is assignable to anotherCanSerialize = IsAssignable[User, Serializable] # True or False at type level
# Use in conditional typesHandler = If[ IsAssignable[Model, Serializable], ModelSerializer[Model], DictSerializer[Model]]I haven’t found as many use cases for IsEquivalent, but it’s useful for type-level assertions:
from typing import IsEquivalent, assert_type
# Ensure two types are identical (type checker error if not)assert IsEquivalent[ProcessedModel, ExpectedModel]Extended Callables for Method Types
Another useful addition is ExtendedCallable, which captures the self or cls parameter:
from typing import ExtendedCallable, Self
class Service: def process(self, data: str) -> int: return len(data)
# Type-safe callable referenceProcessFunc = ExtendedCallable[[Self, str], int]
# This type now correctly includes the self parameterdef register_handler(func: ProcessFunc) -> None: ...Before this, I had to use Callable[[str], int] and lose the information about which class the method belonged to. Now the type system tracks that.
What This Enables for Frameworks
The real power of PEP 827 becomes clear when you look at what framework authors can now do:
ORM Type Safety (Prisma-style):
# Define schema onceclass UserSchema: id: int name: str email: str
# Framework derives:# - User (with runtime methods)# - UserCreate (for insertions)# - UserUpdate (for updates)# - UserWhere (for queries)# - UserInclude (for relations)FastAPI CRUD Generation:
from typing import Members, Optional
def generate_crud_routes[Model](model: type[Model]) -> APIRouter: """Generate CRUD routes from model type.""" router = APIRouter()
CreateDTO = {k: v for k, v in Members[Model].items() if k != "id"} UpdateDTO = {k: Optional[v] for k, v in Members[Model].items()}
@router.post("/") def create(data: CreateDTO) -> Model: ...
@router.patch("/{id}") def update(id: int, data: UpdateDTO) -> Model: ...
return routerGetting Started with PEP 827
As of March 2026, PEP 827 is still rolling out to type checkers. Here’s the current status:
Type Checker Support:
- pyright: Partial support in latest versions
- mypy: Implementation in progress
- pyre: Planning stage
To experiment today:
# Install pyright (best PEP 827 support currently)npm install -g pyright
# Or use the VS Code Pylance extensionPython version: PEP 827 targets Python 3.14+, but you can use typing_extensions for forward compatibility:
from typing_extensions import If, Members, Optional # Works on Python 3.11+Common Patterns I’ve Found Useful
After using PEP 827 for a few months, here are patterns I reach for regularly:
1. Optional variants:
OptionalModel = {k: Optional[v] for k, v in Members[Model].items()}2. Field exclusion:
PublicModel = {k: v for k, v in Members[Model].items() if not k.startswith("_")}3. Type narrowing:
NarrowedType = If[IsAssignable[T, BaseModel], T, Never]4. Configuration extraction:
ConfigFromEnv = {k: v for k, v in Members[Settings].items() if k in os.environ}Limitations and Gotchas
PEP 827 isn’t perfect. Here are issues I’ve encountered:
Runtime Evaluation: Not all type checkers support runtime evaluation of type expressions. If you need to inspect types at runtime (for serialization, validation), you may need typing.get_type_hints() workarounds.
Error Messages: When a type transformation fails, error messages can be cryptic. I’ve seen errors like “type expression is not valid” without details about which part failed.
Learning Curve: The concept of “type-level programming” takes time to internalize. Start with simple comprehensions before attempting complex conditional types.
Why This Matters for Python
PEP 827 transforms Python’s type system from an annotation tool into a type-level programming language. This matters because:
- Less boilerplate: Write types once, derive variants automatically
- Fewer bugs: Type transformations are validated at compile time
- Better tooling: IDEs can understand complex type relationships
- Framework superpowers: Pydantic, FastAPI, and ORMs can derive types automatically
For me, the biggest win is that I now write type definitions once. When I add a field to my model, the create, update, and response variants update automatically. That alone has saved me hours of tedious work and eliminated an entire class of bugs.
Next Steps
If you’re interested in PEP 827:
- Read the full PEP 827 specification
- Install pyright and experiment with conditional types
- Try deriving CRUD models from a base model using unpacked comprehensions
- Watch for mypy and pyright updates supporting the full specification
The type manipulation features in PEP 827 represent a significant shift in how we think about Python types. Instead of just annotating existing code, we can now write code that generates types programmatically. For anyone building frameworks or working with complex domain models, this is a game-changer.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 PEP 827 Specification
- 👨💻 Python Type Hints Documentation
- 👨💻 mypy Type Checker
- 👨💻 Pyright Type Checker
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments