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:
interface User { id: number; name: string; email: string; age: number;}
// Pick only id and nametype UserPreview = Pick<User, 'id' | 'name'>;
// Omit sensitive fieldstype SafeUser = Omit<User, 'email'>;
// Make all fields optionaltype PartialUser = Partial<User>;When I tried to do the same in Python, I got stuck:
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
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: intThis 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
from typing import TypedDict
class UserBase(TypedDict): id: int name: str
class UserFull(UserBase): email: str age: intThis 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:
| Primitive | Purpose |
|---|---|
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.
from typing import TypeVar, Unionfrom 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:
Members[T]- Gets all members of typeTIter[Members[T]]- Iterates over those membersIsAssignable[p.name, Keys]- Checks if the member name matches one of our target keysNewProtocol[...]- Constructs a new type from the filtered members
Now I can use it:
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.
from typing import TypeVar, Unionfrom 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.
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.
from typing import TypeVar, Unionfrom 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.
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
| Feature | TypeScript | Python (with PEP 827) |
|---|---|---|
| Pick | Pick<T, K> | Pick[T, K] |
| Omit | Omit<T, K> | Omit[T, K] |
| Partial | Partial<T> | Partial[T] |
| keyof | keyof T | Members[T] (similar) |
| Union keys | 'a' | 'b' | "a" | "b" |
| Type guards | if ('field' in obj) | Type narrowing |
Real-World Example: API Response Types
Here’s how I use these utility types in a real project:
from typing import TypedDictfrom 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:
- 👨💻 PEP 827 - Type Manipulation
- 👨💻 Reddit Discussion: PEP 827 Type Manipulation
- 👨💻 TypeScript Utility Types Documentation
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments