🐍 Python for TypeScript developers

On this page

A guide to Python for developers coming from TypeScript/JavaScript.

Key differences from TypeScript Jump to heading

Concept TypeScript Python
Typing Static, structural Dynamic (optional type hints)
Indentation Braces {} Whitespace-significant
Compilation Transpiles to JS Interpreted
Package manager npm/yarn/pnpm pip, poetry, uv
Null null / undefined None
Concurrency Async/await, event loop asyncio, threading, multiprocessing

Variables and constants Jump to heading

TypeScript Jump to heading

const name: string = "Alice"
let age: number = 30
const isActive = true

Python Jump to heading

# No const keyword - convention is UPPER_CASE for constants
name: str = "Alice"
age: int = 30
is_active = True # Type inference

# Convention for constants
MAX_SIZE = 100
API_URL = "https://api.example.com"

Basic types Jump to heading

TypeScript Python
string str
number int, float
boolean bool (True/False)
any Any (from typing)
null / undefined None
Array<T> / T[] list[T]
Record<K, V> dict[K, V]
Set<T> set[T]
[T, U] (tuple) tuple[T, U]

Type hints (like TypeScript types) Jump to heading

from typing import Optional, Union, List, Dict, Any

# Basic types
name: str = "Alice"
age: int = 30
scores: list[float] = [95.5, 87.0]
config: dict[str, Any] = {"debug": True}

# Optional (like T | undefined)
email: Optional[str] = None # or str | None (Python 3.10+)

# Union types
id: Union[int, str] = 123 # or int | str (Python 3.10+)

# Type aliases
UserId = int | str
UserMap = dict[str, "User"]

Functions Jump to heading

TypeScript Jump to heading

function greet(name: string): string {
return `Hello, ${name}!`
}

const add = (a: number, b: number): number => a + b

// Optional and default parameters
function log(message: string, level: string = "INFO"): void {
console.log(`[${level}] ${message}`)
}

Python Jump to heading

def greet(name: str) -> str:
return f"Hello, {name}!"

# Lambda (limited to single expression)
add = lambda a, b: a + b

# Default parameters
def log(message: str, level: str = "INFO") -> None:
print(f"[{level}] {message}")

# *args and **kwargs (like rest/spread)
def func(*args, **kwargs):
print(args) # tuple of positional args
print(kwargs) # dict of keyword args

# Keyword-only arguments (after *)
def fetch(url: str, *, timeout: int = 30, retry: bool = True):
pass

fetch("http://...", timeout=60) # Must use keyword

Classes Jump to heading

TypeScript Jump to heading

class User {
private id: number
public name: string

constructor(id: number, name: string) {
this.id = id
this.name = name
}

greet(): string {
return `Hello, ${this.name}`
}

static create(name: string): User {
return new User(Date.now(), name)
}
}

Python Jump to heading

class User:
# Class variable (shared across instances)
count: int = 0

def __init__(self, id: int, name: str):
self.id = id # Instance variable (public)
self.name = name
self._email = "" # Convention: "private" (still accessible)
self.__secret = "" # Name-mangled (harder to access)
User.count += 1

def greet(self) -> str:
return f"Hello, {self.name}"

@staticmethod
def validate(name: str) -> bool:
return len(name) > 0

@classmethod
def create(cls, name: str) -> "User":
return cls(id(name), name)

@property
def email(self) -> str:
return self._email

@email.setter
def email(self, value: str):
self._email = value.lower()

# Usage
user = User(1, "Alice")
user.email = "ALICE@example.com"

Dataclasses (like TypeScript interfaces + classes) Jump to heading

from dataclasses import dataclass, field
from typing import Optional

@dataclass
class User:
id: int
name: str
email: Optional[str] = None
tags: list[str] = field(default_factory=list)

# Auto-generates __init__, __repr__, __eq__
user = User(id=1, name="Alice")
print(user) # User(id=1, name='Alice', email=None, tags=[])

# Frozen (immutable, like readonly)
@dataclass(frozen=True)
class Point:
x: float
y: float

Interfaces (Protocols) Jump to heading

TypeScript Jump to heading

interface Drawable {
draw(): void
}

interface Resizable {
resize(width: number, height: number): void
}

class Circle implements Drawable, Resizable {
draw() { /* ... */ }
resize(w: number, h: number) { /* ... */ }
}

Python Jump to heading

from typing import Protocol

class Drawable(Protocol):
def draw(self) -> None: ...

class Resizable(Protocol):
def resize(self, width: int, height: int) -> None: ...

# No "implements" needed - structural typing (duck typing)
class Circle:
def draw(self) -> None:
print("Drawing circle")

def resize(self, width: int, height: int) -> None:
pass

# This works because Circle has the required methods
def render(item: Drawable) -> None:
item.draw()

render(Circle()) # ✅ Works

Error handling Jump to heading

TypeScript Jump to heading

try {
const data = await fetchData()
} catch (error) {
if (error instanceof NotFoundError) {
console.log("Not found")
}
throw error
} finally {
cleanup()
}

Python Jump to heading

try:
data = fetch_data()
except NotFoundError:
print("Not found")
except (ValueError, TypeError) as e:
print(f"Invalid: {e}")
except Exception as e:
raise RuntimeError("Failed") from e # Chain exceptions
else:
print("Success!") # Only if no exception
finally:
cleanup()

# Custom exceptions
class ValidationError(Exception):
def __init__(self, field: str, message: str):
self.field = field
super().__init__(f"{field}: {message}")

raise ValidationError("email", "Invalid format")

Arrays (Lists) Jump to heading

TypeScript Jump to heading

const arr: number[] = [1, 2, 3]
arr.push(4)
const doubled = arr.map(n => n * 2)
const evens = arr.filter(n => n % 2 === 0)
const sum = arr.reduce((a, b) => a + b, 0)
const first = arr[0]
const last = arr.at(-1)

Python Jump to heading

arr: list[int] = [1, 2, 3]
arr.append(4)

# List comprehensions (preferred over map/filter)
doubled = [n * 2 for n in arr]
evens = [n for n in arr if n % 2 == 0]
sum_val = sum(arr)

# Indexing
first = arr[0]
last = arr[-1] # Negative indexing built-in!

# Slicing
arr[1:3] # [2, 3]
arr[::2] # [1, 3] - every 2nd element
arr[::-1] # [3, 2, 1] - reversed

# Unpacking (like destructuring)
first, *rest = arr # first=1, rest=[2,3]
first, *middle, last = [1, 2, 3, 4] # first=1, middle=[2,3], last=4

# Spread equivalent
combined = [*arr, 4, 5]

# Map/filter with functions
doubled = list(map(lambda n: n * 2, arr))
evens = list(filter(lambda n: n % 2 == 0, arr))

Dictionaries (Objects) Jump to heading

TypeScript Jump to heading

const scores: Record<string, number> = {
alice: 100,
bob: 85
}
scores["charlie"] = 90
scores.alice
delete scores.bob
Object.keys(scores)
Object.entries(scores)

Python Jump to heading

scores: dict[str, int] = {
"alice": 100,
"bob": 85
}

# Access
scores["charlie"] = 90
scores["alice"]
scores.get("david", 0) # Default value if missing

# Delete
del scores["bob"]
scores.pop("bob", None) # Delete with default

# Methods
scores.keys()
scores.values()
scores.items() # Like Object.entries()

# Iteration
for name, score in scores.items():
print(f"{name}: {score}")

# Dict comprehension
doubled = {k: v * 2 for k, v in scores.items()}

# Merge (spread equivalent)
merged = {**scores, "david": 70}
merged = scores | {"david": 70} # Python 3.9+

# Check key exists
if "alice" in scores:
print("Found!")

Async/Await Jump to heading

TypeScript Jump to heading

async function fetchData(url: string): Promise<Response> {
const response = await fetch(url)
return response
}

const results = await Promise.all([
fetchData("url1"),
fetchData("url2")
])

Python Jump to heading

import asyncio
import aiohttp # pip install aiohttp

async def fetch_data(url: str) -> str:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()

# Promise.all equivalent
results = await asyncio.gather(
fetch_data("url1"),
fetch_data("url2")
)

# Run async code
asyncio.run(main())

# Create tasks (fire and forget, then await)
async def main():
task1 = asyncio.create_task(fetch_data("url1"))
task2 = asyncio.create_task(fetch_data("url2"))
results = await asyncio.gather(task1, task2)

Modules and imports Jump to heading

TypeScript Jump to heading

// Named exports
export const foo = 1
export function bar() {}

// Default export
export default class MyClass {}

// Import
import MyClass, { foo, bar } from './module'
import * as utils from './utils'

Python Jump to heading

# my_module.py
foo = 1

def bar():
pass

class MyClass:
pass

# Import
from my_module import foo, bar, MyClass
import my_module
from my_module import * # Not recommended

# Aliasing
from my_module import foo as my_foo
import numpy as np

# Relative imports (in packages)
from . import sibling
from ..parent import something

Package management Jump to heading

TypeScript Jump to heading

npm init
npm install express
npm install -D typescript

Python Jump to heading

# pip (built-in)
pip install requests
pip install -r requirements.txt
pip freeze > requirements.txt

# poetry (recommended for projects)
poetry init
poetry add requests
poetry add --group dev pytest

# uv (fast, modern)
uv init
uv add requests
uv add --dev pytest
# requirements.txt
requests>=2.28.0
pydantic>=2.0.0
# pyproject.toml (modern)
[project]
name = "myproject"
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = ["pytest", "mypy"]

JSON handling Jump to heading

TypeScript Jump to heading

const json = JSON.stringify(obj)
const parsed = JSON.parse(json)

Python Jump to heading

import json

# Stringify
json_str = json.dumps(obj)
json.dumps(obj, indent=2) # Pretty print

# Parse
data = json.loads(json_str)

# File I/O
with open("data.json") as f:
data = json.load(f)

with open("output.json", "w") as f:
json.dump(obj, f, indent=2)

# With Pydantic (like Zod for validation)
from pydantic import BaseModel

class User(BaseModel):
id: int
name: str
email: str | None = None

user = User.model_validate_json('{"id": 1, "name": "Alice"}')
json_str = user.model_dump_json()

HTTP server (FastAPI) Jump to heading

TypeScript (Express) Jump to heading

import express from 'express'

const app = express()
app.use(express.json())

app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, name: 'Alice' })
})

app.post('/users', (req, res) => {
const { name, email } = req.body
res.status(201).json({ id: 1, name, email })
})

app.listen(3000)

Python (FastAPI) Jump to heading

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class UserCreate(BaseModel):
name: str
email: str | None = None

class User(BaseModel):
id: int
name: str
email: str | None = None

@app.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
return User(id=user_id, name="Alice")

@app.post("/users", status_code=201)
async def create_user(user: UserCreate) -> User:
return User(id=1, **user.model_dump())

# Run with: uvicorn main:app --reload

Testing Jump to heading

TypeScript (Jest) Jump to heading

describe('math', () => {
it('adds numbers', () => {
expect(add(1, 2)).toBe(3)
})

it('throws on invalid input', () => {
expect(() => divide(1, 0)).toThrow()
})
})

Python (pytest) Jump to heading

# test_math.py
import pytest

def test_add():
assert add(1, 2) == 3

def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(1, 0)

# Parametrized tests
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
])
def test_add_params(a, b, expected):
assert add(a, b) == expected

# Fixtures (like beforeEach)
@pytest.fixture
def user():
return User(id=1, name="Test")

def test_greet(user):
assert user.greet() == "Hello, Test"
pytest
pytest -v --cov=myapp

String formatting Jump to heading

TypeScript Jump to heading

const name = "Alice"
const message = `Hello, ${name}!`

Python Jump to heading

name = "Alice"

# f-strings (recommended, Python 3.6+)
message = f"Hello, {name}!"
f"2 + 2 = {2 + 2}"
f"Pi: {3.14159:.2f}" # Formatting: "Pi: 3.14"

# .format() method
"Hello, {}!".format(name)
"Hello, {name}!".format(name=name)

# Multiline strings
query = """
SELECT *
FROM users
WHERE id = {id}
"""
.format(id=1)

# Raw strings (like String.raw)
path = r"C:\Users\name" # Backslashes not escaped

Common gotchas for TS developers Jump to heading

  1. Indentation matters - use 4 spaces (PEP 8 standard)
  2. No braces - blocks defined by indentation only
  3. self is explicit - must be first parameter in methods
  4. Mutable default arguments - never use def f(items=[]):
  5. is vs == - is checks identity, == checks equality
  6. Everything is an object - functions, classes, modules
  7. No ++/-- - use += 1 or -= 1
  8. Truthiness differs - empty collections are falsy
# Mutable default argument gotcha
def bad(items=[]): # ❌ Same list reused!
items.append(1)
return items

def good(items=None): # ✅
if items is None:
items = []
items.append(1)
return items

Resources Jump to heading


← Back home