Skip to content

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:

models.py
from pydantic import BaseModel
from datetime import datetime
from 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 model
class UserResponse(BaseModel):
id: int
name: str
email: str

Every 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:

conditional_types.py
from typing import If, IsAssignable
# Define a serializable protocol
class Serializable:
def to_dict(self) -> dict: ...
# Select type based on condition
ResponseType = If[
IsAssignable[Model, Serializable],
Model, # If Model is Serializable
dict[str, Any] # Otherwise
]
def process(model: Model) -> ResponseType:
return model # Type checker validates this

I 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:

unpacked_comprehensions.py
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 update
UpdateFields = {k: Optional[v] for k, v in Members[User].items()}
# Result: {"id": Optional[int], "name": Optional[str], ...}
# Exclude auto-generated fields
CreateFields = {
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:

member_access.py
from typing import GetMemberType, Members, Attrs
class Config:
database_url: str
debug: bool
max_connections: int
# Get specific member type
UrlType = GetMemberType[Config, "database_url"] # str
# Get all attributes as a mapping
ConfigAttrs = Attrs[Config]
# {"database_url": str, "debug": bool, "max_connections": int}
# Iterate over members
for 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:

auto_crud.py
from typing import Members, Optional, TypeVar
from pydantic import BaseModel
from 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 automatically
class 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:

type_operators.py
from typing import IsAssignable, IsEquivalent
# Check if one type is assignable to another
CanSerialize = IsAssignable[User, Serializable] # True or False at type level
# Use in conditional types
Handler = 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:

type_equality.py
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:

extended_callables.py
from typing import ExtendedCallable, Self
class Service:
def process(self, data: str) -> int:
return len(data)
# Type-safe callable reference
ProcessFunc = ExtendedCallable[[Self, str], int]
# This type now correctly includes the self parameter
def 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):

orm_types.py
# Define schema once
class 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:

fastapi_crud.py
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 router

Getting 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.sh
# Install pyright (best PEP 827 support currently)
npm install -g pyright
# Or use the VS Code Pylance extension

Python version: PEP 827 targets Python 3.14+, but you can use typing_extensions for forward compatibility:

compatibility.py
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:

optional_variant.py
OptionalModel = {k: Optional[v] for k, v in Members[Model].items()}

2. Field exclusion:

field_exclusion.py
PublicModel = {k: v for k, v in Members[Model].items() if not k.startswith("_")}

3. Type narrowing:

type_narrowing.py
NarrowedType = If[IsAssignable[T, BaseModel], T, Never]

4. Configuration extraction:

config_extraction.py
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:

  1. Less boilerplate: Write types once, derive variants automatically
  2. Fewer bugs: Type transformations are validated at compile time
  3. Better tooling: IDEs can understand complex type relationships
  4. 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:

  1. Read the full PEP 827 specification
  2. Install pyright and experiment with conditional types
  3. Try deriving CRUD models from a base model using unpacked comprehensions
  4. 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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments