Skip to content

Functions

You already know functions well from TypeScript. Python’s def keyword maps almost directly onto TS’s function declaration — same ideas, slightly different syntax. Where Python diverges is in its keyword arguments system and the ability to return multiple values cleanly via tuple unpacking.

TypeScript
// TypeScript
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
const add = (a: number, b: number): number => a + b;
console.log(greet("Alice")); // Hello, Alice!
console.log(greet("Bob", "Hi")); // Hi, Bob!
console.log(add(3, 4)); // 7
Python
# Python
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
def add(a: int, b: int) -> int:
return a + b
print(greet("Alice")) # Hello, Alice!
print(greet("Bob", "Hi")) # Hi, Bob!
print(add(3, 4)) # 7

Key differences to notice:

  • def keyword instead of function.
  • Return type annotation after -> instead of : ReturnType.
  • No curly braces — the function body is indented.
  • No semicolons.

Python lets callers name any argument at the call site, regardless of position. This is not the same as TypeScript’s object-destructuring pattern — in Python, every parameter is inherently callable by name.

TypeScript
// TypeScript — must use object destructuring to name args
function createUser({
name,
role = "viewer",
active = true,
}: {
name: string;
role?: string;
active?: boolean;
}) {
console.log(`${name} (${role}), active=${active}`);
}
createUser({ name: "Alice", role: "admin" });
Python
# Python — every parameter is callable by name
def create_user(name: str, role: str = "viewer", active: bool = True) -> None:
print(f"{name} ({role}), active={active}")
# Positional call
create_user("Alice", "admin")
# Keyword call — order does not matter
create_user(role="admin", name="Alice")
# Mix positional and keyword
create_user("Bob", active=False)

Python’s *args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dict. Together they let you write flexible APIs.

TypeScript
// TypeScript
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// No clean native equivalent for named variadic args
function logFields(fields: Record<string, string>): void {
for (const [k, v] of Object.entries(fields)) {
console.log(`${k}: ${v}`);
}
}
console.log(sum(1, 2, 3, 4)); // 10
logFields({ lang: "Python", tier: "backend" });
Python
# Python *args and **kwargs
def total(*numbers: float) -> float:
return sum(numbers)
def log_fields(**fields: str) -> None:
for key, value in fields.items():
print(f"{key}: {value}")
print(total(1, 2, 3, 4)) # 10.0
log_fields(lang="Python", tier="backend")

TypeScript uses destructuring on arrays or objects. Python uses tuple unpacking — the function returns a tuple and you unpack it directly. This feels like a first-class language feature rather than a workaround.

TypeScript
// TypeScript — return an object or array
function minMax(nums: number[]): { min: number; max: number } {
return { min: Math.min(...nums), max: Math.max(...nums) };
}
const { min, max } = minMax([3, 1, 4, 1, 5, 9]);
console.log(min, max); // 1 9
Python
# Python — return a tuple, unpack at the call site
def min_max(nums: list[float]) -> tuple[float, float]:
return min(nums), max(nums)
low, high = min_max([3, 1, 4, 1, 5, 9])
print(low, high) # 1 9
# Or keep it as a tuple
result = min_max([3, 1, 4, 1, 5, 9])
print(result) # (1, 9)

Generic functions: type parameters in the signature

Section titled “Generic functions: type parameters in the signature”

TypeScript puts type parameters in angle brackets after the function name. Since Python 3.12 (PEP 695) Python puts them in square brackets in the same place — no TypeVar import, no module-level declaration.

TypeScript
// TypeScript
function first<T>(items: T[]): T | undefined {
return items[0];
}
type Pair<T> = [T, T];
const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefined
Python
# Python 3.12+
def first[T](items: list[T]) -> T | None:
return items[0] if items else None
type Pair[T] = tuple[T, T]
n = first([1, 2, 3]) # int | None
s = first(["a", "b"]) # str | None
print(first([1, 2, 3]), first(["a", "b"]), first([]))

Older code you will still meet declares the variable separately with T = TypeVar("T") and annotates with List[T] from typing. Both still work; the bracket syntax is the one to write in new code.

def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
def stats(*numbers: float) -> tuple[float, float]:
return min(numbers), max(numbers)
def describe(**info: str) -> None:
for key, value in info.items():
print(f" {key}: {value}")
print(greet("Alice"))
print(greet("Bob", greeting="Hi"))
print(greet(name="Carol", greeting="Hey"))
low, high = stats(3, 1, 4, 1, 5, 9, 2, 6)
print(f"min={low}, max={high}")
print("User info:")
describe(language="Python", version="3.14", tier="backend")
What keyword does Python use to define a function?
In Python, calling `greet(name="Alice", greeting="Hey")` despite declaring `def greet(greeting, name)` is valid because of:
What does `**kwargs` collect inside a Python function?
What does `def divmod_custom(a, b): return a // b, a % b` return?