Skip to content

How to Implement TypeScript Utility Types (Pick, Omit, Partial) in Python

I switched from TypeScript to Python recently, and I immediately hit a wall. I couldn’t express myself in Python the way I was used to in TypeScript.

The problem? Python’s type system felt… incomplete. In TypeScript, I could create precise types by picking specific properties, omitting others, or making everything optional. Python’s TypedDict and dataclass couldn’t give me that flexibility.

Then I found PEP 827.

The Problem: Type Manipulation in Python

Let me show you what I mean. In TypeScript, I commonly use these utility types:

user_types.ts
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Pick only id and name
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit sensitive fields
type SafeUser = Omit<User, 'email'>;
// Make all fields optional
type PartialUser = Partial<User>;

When I tried to do the same in Python, I got stuck:

naive_approach.py
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
age: int
# How do I create UserPreview?
# How do I omit email?
# How do I make everything optional?

I tried writing the same types manually, but that’s error-prone and defeats the purpose of having a type system. If I change User, I’d need to update all the derived types too.

The Old Workarounds (And Why They Suck)

Approach 1: Manual Type Definitions

manual_types.py
from typing import TypedDict, Optional
class User(TypedDict):
id: int
name: str
email: str
age: int
class UserPreview(TypedDict):
id: int
name: str
class SafeUser(TypedDict):
id: int
name: str
age: int
class PartialUser(TypedDict, total=False):
id: int
name: str
email: str
age: int

This works, but it’s painful. Every time I add a field to User, I need to manually update UserPreview, SafeUser, and PartialUser. That’s not DRY, and I’ll forget something eventually.

Approach 2: Inheritance Hacks

inheritance_hack.py
from typing import TypedDict
class UserBase(TypedDict):
id: int
name: str
class UserFull(UserBase):
email: str
age: int

This works for Pick in simple cases, but what about Omit? What if I want to exclude the middle field? Inheritance doesn’t help here.

I kept thinking: “Python’s type system is a long way from TypeScript’s keyof.”

PEP 827: The Solution

PEP 827 introduces type manipulation primitives that let us build these utility types. The key components are:

PrimitivePurpose
Members[T]Get the members of a type as a sequence
Attrs[T]Get the attributes of a class
Member[name, type, quals]Construct a member with name, type, and qualifiers
IsAssignable[A, B]Check if A is assignable to B
NewProtocol[...]Create a new protocol type from members

With these, I can finally implement Pick, Omit, and Partial.

Implementing Pick

Pick[T, Keys] filters properties by name, keeping only those specified in Keys.

pick_type.py
from typing import TypeVar, Union
from pep827 import Members, IsAssignable, NewProtocol, Iter
type Pick[T, Keys] = NewProtocol[
*[p for p in Iter[Members[T]] if IsAssignable[p.name, Keys]]
]

Let me break this down:

  1. Members[T] - Gets all members of type T
  2. Iter[Members[T]] - Iterates over those members
  3. IsAssignable[p.name, Keys] - Checks if the member name matches one of our target keys
  4. NewProtocol[...] - Constructs a new type from the filtered members

Now I can use it:

pick_example.py
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
age: int
type UserPreview = Pick[User, "id" | "name"]
# Result: { id: int, name: str }
def get_preview(user: User) -> UserPreview:
return {"id": user["id"], "name": user["name"]}

Implementing Omit

Omit[T, Keys] excludes specific fields from a type.

omit_type.py
from typing import TypeVar, Union
from pep827 import Members, IsAssignable, NewProtocol, Iter
type Omit[T, Keys] = NewProtocol[
*[p for p in Iter[Members[T]] if not IsAssignable[p.name, Keys]]
]

The only difference from Pick is the not in the condition. We keep members whose names are not in Keys.

omit_example.py
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
age: int
type SafeUser = Omit[User, "email">
# Result: { id: int, name: str, age: int }
def sanitize(user: User) -> SafeUser:
return {
"id": user["id"],
"name": user["name"],
"age": user["age"]
}

Implementing Partial

Partial[T] makes all fields in a type optional.

partial_type.py
from typing import TypeVar, Union
from pep827 import Attrs, Member, NewProtocol, Iter
type Partial[T] = NewProtocol[
*[Member[p.name, p.type | None, p.quals] for p in Iter[Attrs[T]]]
]

This one is different. Instead of Members, I use Attrs[T] because I need access to the qualifiers (p.quals). Then I construct new members with the same name and type, but make them optional by adding | None.

partial_example.py
from typing import TypedDict
class User(TypedDict):
id: int
name: str
email: str
age: int
type PartialUser = Partial[User]
# Result: All fields optional (id: int | None, name: str | None, etc.)
def update_user(user: User, updates: PartialUser) -> User:
return {**user, **{k: v for k, v in updates.items() if v is not None}}

TypeScript vs Python: Feature Comparison

FeatureTypeScriptPython (with PEP 827)
PickPick&lt;T, K&gt;Pick[T, K]
OmitOmit&lt;T, K&gt;Omit[T, K]
PartialPartial&lt;T&gt;Partial[T]
keyofkeyof TMembers[T] (similar)
Union keys'a' | 'b'"a" | "b"
Type guardsif ('field' in obj)Type narrowing

Real-World Example: API Response Types

Here’s how I use these utility types in a real project:

api_types.py
from typing import TypedDict
from pep827_utils import Pick, Omit, Partial
class APIResponse(TypedDict):
id: str
status: int
data: dict
error: str | None
timestamp: int
metadata: dict
# Public response (omit internal fields)
type PublicResponse = Omit[APIResponse, "metadata" | "timestamp"]
# Create request (omit auto-generated fields)
type CreateRequest = Omit[APIResponse, "id" | "timestamp">
# Update request (partial, all optional)
type UpdateRequest = Partial[Omit[APIResponse, "id" | "timestamp"]]
def handle_response(response: APIResponse) -> PublicResponse:
return {
"id": response["id"],
"status": response["status"],
"data": response["data"],
"error": response["error"]
}

The Trade-offs

PEP 827 is powerful, but there are some caveats:

Pros:

  • DRY principle - derived types stay in sync
  • Type-safe refactoring
  • Expressive type system closer to TypeScript

Cons:

  • PEP 827 is still experimental (as of March 2026)
  • Error messages can be cryptic
  • IDE support is limited compared to TypeScript
  • Runtime overhead for complex type operations

Conclusion

I can finally express myself in Python the way I was used to in TypeScript. PEP 827 bridges the gap between Python’s dynamic nature and TypeScript’s powerful type manipulation.

If you’re coming from TypeScript and feeling constrained by Python’s type system, give PEP 827 a try. The learning curve is worth it for the type safety and expressiveness you gain.

The type manipulation primitives (Members, Attrs, Member, IsAssignable, NewProtocol) give you the building blocks to create your own utility types beyond just Pick, Omit, and Partial. I’ve already started implementing Required (the opposite of Partial) and Record<K, T> (like TypeScript’s record type).

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