Skip to content

Typing & Protocols

TypeScript interfaces → Python Protocols

Section titled “TypeScript interfaces → Python Protocols”

In TypeScript, an interface defines a contract. Any object that has the required shape satisfies it — this is called structural typing. Python’s Protocol (from the typing module) is exactly the same idea: a class satisfies a Protocol if it has the right methods and attributes, regardless of what it inherits from.

TypeScript
// TypeScript structural interface
interface Drawable {
draw(): string;
}
function render(shape: Drawable): string {
return shape.draw();
}
class Circle {
draw() { return "Drawing Circle"; }
}
class Square {
draw() { return "Drawing Square"; }
}
console.log(render(new Circle())); // "Drawing Circle"
console.log(render(new Square())); // "Drawing Square"
Python
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
def render(shape: Drawable) -> str:
return shape.draw()
class Circle:
def draw(self) -> str:
return "Drawing Circle"
class Square:
def draw(self) -> str:
return "Drawing Square"
print(render(Circle())) # Drawing Circle
print(render(Square())) # Drawing Square

Notice Circle and Square do not inherit from Drawable. They just happen to have a draw() method with the right signature — that is enough. Static analysis tools like mypy and pyright will catch violations.

TypeScript generics map directly to Python’s TypeVar + Generic.

TypeScript
// TypeScript generic class
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T { return this.items.pop()!; }
peek(): T { return this.items[this.items.length - 1]; }
}
const stack = new Stack<number>();
stack.push(1);
stack.push(2);
console.log(stack.pop()); // 2
Python
from typing import TypeVar, Generic
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
print(stack.pop()) # 2

From Python 3.9+ you can also write list[T] directly in class bodies without importing List from typing.

PEP 695: the syntax that finally looks like TypeScript

Section titled “PEP 695: the syntax that finally looks like TypeScript”

Python 3.12 added declaration-site type parameters, so a generic class or function no longer needs a module-level TypeVar at all. The scoping is implicit, exactly like class Stack<T> in TypeScript.

TypeScript
// TypeScript — parameters live on the declaration
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
}
function first<T>(items: T[]): T { return items[0]; }
type Pair<T> = [T, T];
Python
# Python 3.12+ — PEP 695, no TypeVar import
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def first[T](items: list[T]) -> T:
return items[0]
type Pair[T] = tuple[T, T] # type alias statement
# Bounds and constraints go inline too:
def largest[T: (int, float)](values: list[T]) -> T:
return max(values)

TypeVar still works and is what you will see in libraries that support older Pythons, so read both. New code on 3.12+ should prefer the bracket syntax. Python 3.13 also gave type parameters defaults (PEP 696) — TypeVar('T', default=int), or class Box[T = int] — the counterpart of class Box<T = number>.

TypedDict is Python’s equivalent of a TypeScript interface that describes a plain object (no methods). It is perfect for JSON-shaped data.

TypeScript
// TypeScript interface for a plain object
interface UserRecord {
name: string;
age: number;
email?: string; // optional key
}
const user: UserRecord = { name: "Alice", age: 30 };
console.log(user.name);
Python
from typing import TypedDict, NotRequired
class UserRecord(TypedDict):
name: str
age: int
email: NotRequired[str] # optional key
user: UserRecord = {"name": "Alice", "age": 30}
print(user["name"]) # Alice
# mypy/pyright will catch missing required keys:
# bad: UserRecord = {"name": "Alice"} # age missing

Python 3.13 added ReadOnly for TypedDict — the analogue of TypeScript’s readonly property modifier. A type checker then rejects writes to that key while still allowing the rest of the dict to be mutated.

TypeScript
interface Config {
readonly name: string;
retries: number;
}
Python
from typing import TypedDict, ReadOnly
class Config(TypedDict):
name: ReadOnly[str]
retries: int
cfg: Config = {"name": "api", "retries": 3}
cfg["retries"] = 5 # fine
# cfg["name"] = "other" # type checker error

Two more pieces land close to what TypeScript gives you. TypeIs (Python 3.13) is a type predicate that narrows in both branches, which is what TypeScript’s arg is Type already does — the older TypeGuard only narrowed the positive branch. And @override (Python 3.12) asks the checker to verify that a method really does override a base-class method, like TypeScript’s override keyword under noImplicitOverride.

TypeScript
// TypeScript — predicate narrows both branches
function isString(x: unknown): x is string {
return typeof x === "string";
}
class Base { greet(): void {} }
class Child extends Base {
override greet(): void {} // checked
}
Python
from typing import TypeIs, override
def is_string(x: object) -> TypeIs[str]:
return isinstance(x, str)
def handle(value: int | str) -> None:
if is_string(value):
print(value.upper()) # narrowed to str
else:
print(value + 1) # narrowed to int — TypeGuard could not do this
class Base:
def greet(self) -> None: ...
class Child(Base):
@override
def greet(self) -> None: ... # checker verifies the base method exists

TypeScript uses string | null | undefined for nullable types. Python uses Optional[T] (which is T | None) or the modern union syntax T | None (Python 3.10+).

TypeScript
// TypeScript
function greet(name: string | null): string {
if (name === null) return "Hello, stranger!";
return `Hello, ${name}!`;
}
// Modern TS nullable shorthand
function greet2(name?: string): string {
return name ? `Hello, ${name}!` : "Hello, stranger!";
}
Python
from typing import Optional
# Optional[str] is exactly str | None
def greet(name: Optional[str] = None) -> str:
if name is None:
return "Hello, stranger!"
return f"Hello, {name}!"
# Python 3.10+ union syntax
def greet_modern(name: str | None = None) -> str:
return f"Hello, {name}!" if name else "Hello, stranger!"
print(greet()) # Hello, stranger!
print(greet("Alice")) # Hello, Alice!
from typing import Protocol, TypeVar, Generic, TypedDict, Optional
# Protocol — structural typing
class Serializable(Protocol):
def to_json(self) -> str: ...
class User:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def to_json(self) -> str:
return f'{{"name": "{self.name}", "age": {self.age}}}'
class Product:
def __init__(self, title: str) -> None:
self.title = title
def to_json(self) -> str:
return f'{{"title": "{self.title}"}}'
def export_data(item: Serializable) -> str:
return item.to_json()
print(export_data(User("Alice", 30)))
print(export_data(Product("Widget")))
# Generic Stack
T = TypeVar('T')
class Stack(Generic[T]):
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
s: Stack[str] = Stack()
s.push("hello")
s.push("world")
print(s.pop())
# TypedDict
class Config(TypedDict):
host: str
port: int
cfg: Config = {"host": "localhost", "port": 8080}
print(f"Connecting to {cfg['host']}:{cfg['port']}")
# Optional
def find_user(user_id: int) -> Optional[str]:
db = {1: "Alice", 2: "Bob"}
return db.get(user_id)
print(find_user(1)) # Alice
print(find_user(99)) # None
Which Python class serves as the direct equivalent to a TypeScript structural interface?
What does `Optional[str]` mean in Python's typing module?
For Python's Protocols to work with `isinstance()` at runtime, what decorator is required?
Does a class need to explicitly inherit from a `Protocol` to satisfy it?
Which Python version lets you write `class Stack[T]:` without importing TypeVar?
What does TypeIs give you that TypeGuard does not?