Claude Code Plugins

Community-maintained marketplace

Feedback

convert-typescript-python

@aRustyDev/ai
0
0

Convert TypeScript code to idiomatic Python. Use when migrating TypeScript projects to Python, translating TypeScript patterns to Pythonic idioms, or refactoring TypeScript codebases into Python. Extends meta-convert-dev with TypeScript-to-Python specific patterns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name convert-typescript-python
description Convert TypeScript code to idiomatic Python. Use when migrating TypeScript projects to Python, translating TypeScript patterns to Pythonic idioms, or refactoring TypeScript codebases into Python. Extends meta-convert-dev with TypeScript-to-Python specific patterns.

Convert TypeScript to Python

Convert TypeScript code to idiomatic Python. This skill extends meta-convert-dev with TypeScript-to-Python specific type mappings, idiom translations, and tooling.

This Skill Extends

  • meta-convert-dev - Foundational conversion patterns (APTV workflow, testing strategies)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: TypeScript types → Python types (with type hints)
  • Idiom translations: TypeScript patterns → Pythonic idioms
  • Error handling: TypeScript exceptions → Python exceptions (different patterns)
  • Async patterns: Promise/async-await → asyncio
  • Type system differences: Static structural typing → dynamic duck typing

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • TypeScript language fundamentals - see lang-typescript-dev
  • Python language fundamentals - see lang-python-dev
  • Reverse conversion (Python → TypeScript) - see convert-python-typescript

Quick Reference

TypeScript Python Notes
string str Unicode strings
number int / float Specify precision
boolean bool Same concept
Array<T> / T[] list[T] Mutable sequence
readonly T[] tuple[T, ...] Immutable sequence
Record<K, V> dict[K, V] Key-value mapping
T | null T | None / Optional[T] Nullable types
Promise<T> Coroutine[Any, Any, T] Async operations
interface X class X / Protocol Structure definition
enum X Enum / Literal Enumerated values

When Converting Code

  1. Analyze source thoroughly before writing Python
  2. Map types first - TypeScript types → Python type hints
  3. Preserve semantics over syntax similarity
  4. Adopt Python idioms - don't write "TypeScript code in Python syntax"
  5. Handle edge cases - undefined/null → None, falsy values
  6. Test equivalence - same inputs → same outputs

Type System Mapping

Primitive Types

TypeScript Python Notes
string str Unicode by default in both
number int For integers
number float For floating-point
bigint int Python has arbitrary-precision integers
boolean bool Capitalized (True/False) in Python
null None Singleton null value
undefined None Both map to None in Python
void None Function return type
any Any Escape hatch (avoid when possible)
unknown Any / object Prefer object for constraints
never NoReturn Function never returns
symbol - No direct equivalent (use strings)

Collection Types

TypeScript Python Notes
T[] / Array<T> list[T] Mutable, ordered
readonly T[] tuple[T, ...] / Sequence[T] Immutable sequence
[T, U] tuple[T, U] Fixed-size tuple
Set<T> set[T] Unique values, unordered
Map<K, V> dict[K, V] Key-value mapping
Record<K, V> dict[K, V] Type-safe object
WeakMap<K, V> weakref.WeakValueDictionary Weak references
ReadonlyMap<K, V> Mapping[K, V] Immutable view
ReadonlySet<T> frozenset[T] / AbstractSet[T] Immutable set

Composite Types

TypeScript Python Notes
interface X { ... } class X: / TypedDict Data class or typed dict
type X = Y | Z X = Y | Z Union types (3.10+)
type X = Y & Z Multiple inheritance / Protocol Intersection via inheritance
T | null T | None / Optional[T] Nullable types
Partial<T> TypedDict with total=False All fields optional
Required<T> TypedDict with total=True All fields required
Pick<T, K> TypedDict subset Manual definition
Omit<T, K> TypedDict subset Manual definition
ReturnType<T> Function annotations Infer from function signature

Generic Types

TypeScript Python Notes
<T> [T] (3.12+) / TypeVar('T') Generic type parameter
<T extends U> TypeVar('T', bound=U) Bounded type variable
<T extends A | B> TypeVar('T', A, B) Constrained type variable
<T = Default> [T = Default] (3.13+) Default type parameter
keyof T Literal[...] String literal union
T[K] @overload / __getitem__ Index access type

Idiom Translation

Pattern 1: Optional Chaining & Nullish Coalescing

TypeScript:

const name = user?.profile?.name ?? "Anonymous";
const age = user?.age ?? 0;

Python:

name = (user.profile.name if user and user.profile else None) or "Anonymous"
# Better with walrus operator (3.8+)
name = (profile.name if (profile := getattr(user, 'profile', None)) else None) or "Anonymous"

# Or using helper
from operator import attrgetter
name = attrgetter('profile.name')(user) if user else "Anonymous"

# Most Pythonic: use get() for dicts, getattr() for objects
name = getattr(getattr(user, 'profile', None), 'name', None) or "Anonymous"

Why this translation:

  • Python doesn't have built-in optional chaining
  • or operator works for default values but watch for falsy values (0, "", [], etc.)
  • Explicit None checks are more Pythonic than chaining
  • Consider using getattr() with defaults for cleaner code

Pattern 2: Array/List Methods

TypeScript:

const result = items
  .filter(x => x.active)
  .map(x => x.value)
  .reduce((sum, val) => sum + val, 0);

Python:

# List comprehension (most Pythonic)
result = sum(x.value for x in items if x.active)

# Or step by step
result = sum(
    x.value
    for x in items
    if x.active
)

# Functional style (less Pythonic)
from functools import reduce
result = reduce(
    lambda sum, val: sum + val,
    map(lambda x: x.value, filter(lambda x: x.active, items)),
    0
)

Why this translation:

  • List comprehensions and generator expressions are the Pythonic way
  • More readable and often faster than map/filter chains
  • Built-in functions like sum(), any(), all() are preferred
  • Avoid reduce() unless necessary (explicit loops are clearer)

Pattern 3: Object Destructuring

TypeScript:

const { name, age, ...rest } = user;
const [first, second, ...others] = items;

Python:

# Dictionary unpacking
name = user['name']
age = user['age']
rest = {k: v for k, v in user.items() if k not in ('name', 'age')}

# Or with destructuring (dataclasses)
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    extra: dict

# List unpacking
first, second, *others = items

# Named tuple unpacking
from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int

p = Point(1, 2)
x, y = p  # Unpacking

Why this translation:

  • Python has tuple unpacking with * (PEP 3132)
  • Dict comprehension for "rest" pattern
  • Dataclasses/NamedTuples provide structure with unpacking
  • No built-in object rest spread like TypeScript

Pattern 4: Async/Await

TypeScript:

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data;
}

const users = await Promise.all(ids.map(id => fetchUser(id)));

Python:

import asyncio
import aiohttp

async def fetch_user(id: str) -> User:
    async with aiohttp.ClientSession() as session:
        async with session.get(f'/api/users/{id}') as response:
            data = await response.json()
            return User(**data)

# Concurrent execution
users = await asyncio.gather(*[fetch_user(id) for id in ids])

# Or with TaskGroup (3.11+)
async with asyncio.TaskGroup() as tg:
    tasks = [tg.create_task(fetch_user(id)) for id in ids]
users = [task.result() for task in tasks]

Why this translation:

  • Python's async/await is similar but requires explicit event loop
  • asyncio.gather() is equivalent to Promise.all()
  • Context managers (async with) for resource management
  • TaskGroup provides structured concurrency (Python 3.11+)

Pattern 5: Class & Interface

TypeScript:

interface Repository {
  save(item: Item): Promise<void>;
  findById(id: string): Promise<Item | null>;
}

class ItemRepository implements Repository {
  async save(item: Item): Promise<void> {
    // implementation
  }

  async findById(id: string): Promise<Item | null> {
    // implementation
  }
}

Python:

from abc import ABC, abstractmethod
from typing import Protocol, Optional

# Option 1: Protocol (structural typing, like TS interface)
class Repository(Protocol):
    async def save(self, item: Item) -> None: ...
    async def findById(self, id: str) -> Optional[Item]: ...

# Option 2: ABC (nominal typing, explicit inheritance)
class RepositoryBase(ABC):
    @abstractmethod
    async def save(self, item: Item) -> None:
        pass

    @abstractmethod
    async def findById(self, id: str) -> Optional[Item]:
        pass

# Implementation
class ItemRepository:  # Protocol doesn't require inheritance
    async def save(self, item: Item) -> None:
        # implementation
        pass

    async def findById(self, id: str) -> Optional[Item]:
        # implementation
        return None

# Or with ABC
class ItemRepository(RepositoryBase):
    async def save(self, item: Item) -> None:
        pass

    async def findById(self, id: str) -> Optional[Item]:
        return None

Why this translation:

  • Protocol provides structural typing (like TypeScript interfaces)
  • ABC provides nominal typing (explicit inheritance required)
  • Use Protocol for duck typing, ABC for strict contracts
  • Python 3.8+ supports Protocol from typing

Pattern 6: Enum Types

TypeScript:

enum Status {
  Pending = "pending",
  Active = "active",
  Completed = "completed"
}

type StatusType = Status.Pending | Status.Active;

Python:

from enum import Enum, auto
from typing import Literal

# Option 1: Enum class
class Status(Enum):
    PENDING = "pending"
    ACTIVE = "active"
    COMPLETED = "completed"

# Option 2: StrEnum (3.11+)
from enum import StrEnum

class Status(StrEnum):
    PENDING = "pending"
    ACTIVE = "active"
    COMPLETED = "completed"

# Option 3: Literal types
StatusType = Literal["pending", "active", "completed"]

# Subset type
ActiveStatus = Literal["pending", "active"]

# Usage
def process(status: Status) -> None:
    if status == Status.PENDING:
        ...
    elif status is Status.ACTIVE:  # Use 'is' for enum comparison
        ...

# With Literal
def handle(status: StatusType) -> None:
    if status == "pending":
        ...

Why this translation:

  • Enum provides type-safe enumerations with methods
  • Literal types for simple string/int unions
  • StrEnum (Python 3.11+) for string enums that compare as strings
  • Use is for enum comparisons (identity), == for Literal

Pattern 7: Error Handling

TypeScript:

class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
    this.name = "ValidationError";
  }
}

function validateUser(user: User): void {
  if (!user.email) {
    throw new ValidationError("Email required", "email");
  }
}

try {
  validateUser(user);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(`Field ${error.field}: ${error.message}`);
  } else {
    throw error;
  }
}

Python:

class ValidationError(ValueError):
    """Raised when validation fails"""
    def __init__(self, message: str, field: str):
        super().__init__(message)
        self.field = field

def validate_user(user: User) -> None:
    if not user.email:
        raise ValidationError("Email required", field="email")

# Exception handling
try:
    validate_user(user)
except ValidationError as error:
    print(f"Field {error.field}: {error}")
except Exception:
    raise  # Re-raise if not ValidationError

# Context manager for cleanup
from contextlib import contextmanager

@contextmanager
def transaction():
    try:
        yield
    except Exception:
        rollback()
        raise
    else:
        commit()

with transaction():
    validate_user(user)

Why this translation:

  • Both use exception-based error handling
  • Python exceptions inherit from Exception (or specific types)
  • Type checking with isinstance() or specific except clause
  • Context managers (with statement) for resource cleanup
  • Python has no finally equivalent to TS but uses finally block

Pattern 8: Type Guards & Narrowing

TypeScript:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

function process(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase());
  } else {
    console.log(value.toFixed(2));
  }
}

Python:

from typing import TypeGuard, Union

def is_string(value: object) -> TypeGuard[str]:
    return isinstance(value, str)

def process(value: Union[str, float]) -> None:
    if isinstance(value, str):
        print(value.upper())
    else:
        print(f"{value:.2f}")

# Pattern matching (3.10+)
match value:
    case str():
        print(value.upper())
    case float() | int():
        print(f"{value:.2f}")
    case _:
        raise TypeError(f"Unexpected type: {type(value)}")

# Type narrowing with assert
from typing import assert_never

def handle(value: str | int | None) -> None:
    if value is None:
        return
    elif isinstance(value, str):
        print(value.upper())
    elif isinstance(value, int):
        print(value * 2)
    else:
        assert_never(value)  # Exhaustiveness check

Why this translation:

  • TypeGuard (Python 3.10+) for type guard functions
  • isinstance() for runtime type checking with narrowing
  • Pattern matching (3.10+) for structural matching
  • assert_never() for exhaustiveness checking

Pattern 9: Decorators & Metadata

TypeScript:

function logged(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${key} with`, args);
    return original.apply(this, args);
  };
}

class Service {
  @logged
  process(data: string) {
    return data.toUpperCase();
  }
}

Python:

from functools import wraps
from typing import Callable, TypeVar, ParamSpec

P = ParamSpec('P')
T = TypeVar('T')

def logged(func: Callable[P, T]) -> Callable[P, T]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        print(f"Calling {func.__name__} with {args}, {kwargs}")
        return func(*args, **kwargs)
    return wrapper

class Service:
    @logged
    def process(self, data: str) -> str:
        return data.upper()

# Property decorator
class Config:
    def __init__(self, value: int):
        self._value = value

    @property
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, val: int) -> None:
        if val < 0:
            raise ValueError("Must be positive")
        self._value = val

# Class decorator
def singleton(cls):
    instances = {}
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class Database:
    pass

Why this translation:

  • Python decorators use @ syntax like TypeScript
  • @wraps preserves function metadata
  • ParamSpec and TypeVar for type-safe decorators
  • Property decorators for getters/setters
  • Class decorators for metaprogramming

Pattern 10: Modules & Imports

TypeScript:

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export default class Calculator {
  // ...
}

// main.ts
import Calculator, { add } from './math';
import * as math from './math';

Python:

# math.py
def add(a: float, b: float) -> float:
    return a + b

class Calculator:
    pass

# Explicit public API
__all__ = ['add', 'Calculator']

# main.py
from math import add, Calculator  # Named imports
import math  # Module import
from math import *  # Import all (not recommended)

# Conditional imports
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from expensive_module import HeavyClass  # Type-only import

# Lazy import
def process():
    import heavy_module  # Imported only when function called
    return heavy_module.process()

Why this translation:

  • Python has no "default export" - all exports are named
  • __all__ defines public API (like explicit exports)
  • TYPE_CHECKING for type-only imports (avoid circular deps)
  • Python imports are executed once and cached
  • Lazy imports for optional dependencies

Error Handling

TypeScript Error Model → Python Error Model

Both TypeScript and Python use exception-based error handling, but with different patterns and conventions.

Aspect TypeScript Python
Base class Error Exception
Try-catch try/catch/finally try/except/else/finally
Throwing throw new Error() raise Exception()
Re-throwing throw raise (without args)
Type checking instanceof isinstance() or specific except

Exception Hierarchy Translation

TypeScript:

class AppError extends Error {
  constructor(message: string) {
    super(message);
    this.name = this.constructor.name;
  }
}

class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`);
  }
}

class ValidationError extends AppError {
  constructor(message: string, public errors: string[]) {
    super(message);
  }
}

Python:

class AppError(Exception):
    """Base exception for application errors"""
    pass

class NotFoundError(AppError):
    """Raised when a resource is not found"""
    def __init__(self, resource: str):
        super().__init__(f"{resource} not found")
        self.resource = resource

class ValidationError(AppError):
    """Raised when validation fails"""
    def __init__(self, message: str, errors: list[str]):
        super().__init__(message)
        self.errors = errors

# Using standard library base classes
class ConfigError(ValueError):
    """Inherit from ValueError for invalid values"""
    pass

class NetworkError(IOError):
    """Inherit from IOError for I/O failures"""
    pass

Error Handling Patterns

TypeScript:

async function loadConfig(path: string): Promise<Config> {
  try {
    const content = await fs.promises.readFile(path, 'utf-8');
    return JSON.parse(content);
  } catch (error) {
    if (error instanceof SyntaxError) {
      throw new ConfigError(`Invalid JSON in ${path}`, error);
    } else if (error.code === 'ENOENT') {
      throw new NotFoundError(path);
    } else {
      throw error;
    }
  }
}

Python:

import json
from pathlib import Path

async def load_config(path: str) -> Config:
    try:
        content = await asyncio.to_thread(Path(path).read_text)
        data = json.loads(content)
        return Config(**data)
    except json.JSONDecodeError as error:
        raise ConfigError(f"Invalid JSON in {path}") from error
    except FileNotFoundError:
        raise NotFoundError(path) from None
    except Exception:
        raise  # Re-raise unexpected errors

# Exception chaining with 'from'
try:
    result = parse_data(raw)
except ValueError as error:
    raise ValidationError("Invalid data") from error  # Chain exception

# Suppress original exception with 'from None'
try:
    result = fetch_data()
except RequestError:
    raise ServiceUnavailable() from None  # Hide implementation detail

Context Managers for Cleanup

TypeScript:

class Connection {
  async connect(): Promise<void> { /* ... */ }
  async disconnect(): Promise<void> { /* ... */ }

  async withConnection<T>(fn: () => Promise<T>): Promise<T> {
    await this.connect();
    try {
      return await fn();
    } finally {
      await this.disconnect();
    }
  }
}

await connection.withConnection(async () => {
  // use connection
});

Python:

from contextlib import asynccontextmanager

class Connection:
    async def connect(self) -> None:
        pass

    async def disconnect(self) -> None:
        pass

    # Context manager protocol
    async def __aenter__(self):
        await self.connect()
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.disconnect()
        return False  # Don't suppress exceptions

# Usage
async with Connection() as conn:
    # use connection
    pass

# Function-based context manager
@asynccontextmanager
async def get_connection():
    conn = Connection()
    await conn.connect()
    try:
        yield conn
    finally:
        await conn.disconnect()

async with get_connection() as conn:
    # use connection
    pass

Concurrency Patterns

TypeScript Async → Python Asyncio

Both languages support async/await, but Python requires explicit event loop management and has different primitives.

Aspect TypeScript Python
Runtime V8 event loop (built-in) asyncio event loop (explicit)
Promise Promise<T> Coroutine[Any, Any, T]
Concurrent Promise.all() asyncio.gather()
Race Promise.race() asyncio.wait(..., FIRST_COMPLETED)
Sequential await each await each
Timeout Promise.race() + timeout asyncio.wait_for()

Basic Async Functions

TypeScript:

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`Failed to fetch user ${id}`);
  }
  return await response.json();
}

// Sequential
const user1 = await fetchUser('1');
const user2 = await fetchUser('2');

// Concurrent
const [user1, user2] = await Promise.all([
  fetchUser('1'),
  fetchUser('2')
]);

Python:

import aiohttp
import asyncio

async def fetch_user(id: str) -> User:
    async with aiohttp.ClientSession() as session:
        async with session.get(f'/api/users/{id}') as response:
            if not response.ok:
                raise Exception(f"Failed to fetch user {id}")
            data = await response.json()
            return User(**data)

# Sequential
user1 = await fetch_user('1')
user2 = await fetch_user('2')

# Concurrent
user1, user2 = await asyncio.gather(
    fetch_user('1'),
    fetch_user('2')
)

# With TaskGroup (3.11+) - preferred for structured concurrency
async with asyncio.TaskGroup() as tg:
    task1 = tg.create_task(fetch_user('1'))
    task2 = tg.create_task(fetch_user('2'))
user1 = task1.result()
user2 = task2.result()

Async Iteration & Streams

TypeScript:

async function* generatePages(start: number, end: number) {
  for (let i = start; i <= end; i++) {
    yield await fetchPage(i);
  }
}

for await (const page of generatePages(1, 10)) {
  process(page);
}

Python:

from typing import AsyncIterator

async def generate_pages(start: int, end: int) -> AsyncIterator[Page]:
    for i in range(start, end + 1):
        yield await fetch_page(i)

# Async iteration
async for page in generate_pages(1, 10):
    process(page)

# Async comprehension
pages = [page async for page in generate_pages(1, 10)]

# Async generator expression
total = sum(page.size async for page in generate_pages(1, 10))

Timeout & Cancellation

TypeScript:

async function fetchWithTimeout<T>(
  promise: Promise<T>,
  ms: number
): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), ms)
  );
  return Promise.race([promise, timeout]);
}

const user = await fetchWithTimeout(fetchUser('1'), 5000);

Python:

async def fetch_with_timeout(coro, seconds: float):
    try:
        return await asyncio.wait_for(coro, timeout=seconds)
    except asyncio.TimeoutError:
        raise TimeoutError(f"Operation timed out after {seconds}s")

user = await fetch_with_timeout(fetch_user('1'), 5.0)

# Manual timeout with select
async def fetch_user_timeout(id: str, timeout: float) -> User:
    try:
        async with asyncio.timeout(timeout):  # 3.11+
            return await fetch_user(id)
    except asyncio.TimeoutError:
        raise TimeoutError(f"Fetch timed out for user {id}")

Background Tasks

TypeScript:

// Fire and forget (not awaited)
fetchUser('1').catch(error => console.error(error));

// With proper cleanup
const tasks: Promise<void>[] = [];
tasks.push(processItem('1'));
tasks.push(processItem('2'));

await Promise.allSettled(tasks);

Python:

# Fire and forget - create task but don't await
task = asyncio.create_task(fetch_user('1'))

# Add done callback
def handle_error(task: asyncio.Task):
    if task.exception():
        print(f"Error: {task.exception()}")

task.add_done_callback(handle_error)

# Background tasks with tracking
tasks = [
    asyncio.create_task(process_item('1')),
    asyncio.create_task(process_item('2'))
]

# Wait for all, handle errors
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
    if isinstance(result, Exception):
        print(f"Error: {result}")

# Proper cleanup on shutdown
async def shutdown(tasks: list[asyncio.Task]):
    for task in tasks:
        task.cancel()
    await asyncio.gather(*tasks, return_exceptions=True)

Common Pitfalls

1. TypeScript undefined vs null → Python None

Problem: TypeScript distinguishes undefined and null, Python only has None.

// TypeScript
interface User {
  name: string;
  age?: number;  // undefined if not provided
  email: string | null;  // explicitly nullable
}
# Python - both map to None
from typing import Optional

class User:
    name: str
    age: Optional[int] = None  # undefined → None
    email: Optional[str] = None  # null → None

Solution: Document the semantic difference in comments or use sentinel values if distinction is critical.

2. Falsy Values in Conditionals

Problem: Python treats more values as falsy than TypeScript.

// TypeScript - only null/undefined are falsy for objects
if (user) { /* user is not null/undefined */ }
if (count) { /* count is not 0 */ }
# Python - many values are falsy: None, False, 0, "", [], {}, etc.
if user:  # False if None or any "empty" value
    pass

# Be explicit
if user is not None:  # Only checks None
    pass

if count != 0:  # Explicit zero check
    pass

Solution: Use explicit comparisons (is not None, != 0, len(items) > 0) rather than truthy checks when the distinction matters.

3. Mutable Default Arguments

Problem: Python mutable defaults are shared across calls.

// TypeScript - new array each time
function addItem(item: string, list: string[] = []): string[] {
  list.push(item);
  return list;
}
# Python - WRONG: shared across calls
def add_item(item: str, list: list[str] = []) -> list[str]:
    list.append(item)
    return list

# Correct: use None and create new list
def add_item(item: str, list: list[str] | None = None) -> list[str]:
    if list is None:
        list = []
    list.append(item)
    return list

Solution: Never use mutable objects as default arguments. Use None and create the object inside the function.

4. Dictionary Key Errors

Problem: TypeScript objects allow undefined properties, Python dicts raise KeyError.

// TypeScript - returns undefined
const value = obj['nonexistent'];  // undefined
# Python - raises KeyError
value = obj['nonexistent']  # KeyError!

# Use get() with default
value = obj.get('nonexistent')  # None
value = obj.get('nonexistent', 'default')  # 'default'

# Or check first
if 'nonexistent' in obj:
    value = obj['nonexistent']

Solution: Use dict.get() with defaults or check key existence with in operator.

5. Integer Division

Problem: TypeScript / always returns number (float), Python / returns float but // returns int.

// TypeScript
const result = 5 / 2;  // 2.5
# Python - different operators
result = 5 / 2   # 2.5 (float division)
result = 5 // 2  # 2 (floor division)

# Be explicit about intent
import math
result = math.floor(5 / 2)  # Explicit floor
result = int(5 / 2)  # Truncate toward zero

Solution: Use // for integer division, / for float division. Be explicit with math.floor() or int() for clarity.

6. Class Property Initialization

Problem: TypeScript class properties initialize before constructor, Python requires __init__.

// TypeScript
class Counter {
  count = 0;  // Initialized before constructor

  constructor(initial?: number) {
    if (initial !== undefined) {
      this.count = initial;
    }
  }
}
# Python - WRONG
class Counter:
    count = 0  # Class variable (shared across instances!)

    def __init__(self, initial: int | None = None):
        if initial is not None:
            self.count = initial

# Correct - instance variables in __init__
class Counter:
    def __init__(self, initial: int | None = None):
        self.count = initial if initial is not None else 0

# Or use dataclass
from dataclasses import dataclass, field

@dataclass
class Counter:
    count: int = 0

Solution: Always initialize instance variables in __init__. Use @dataclass for simple data classes.

7. Async Event Loop Requirements

Problem: Python requires explicit event loop management.

// TypeScript - just use async/await
async function main() {
  const result = await fetchData();
  return result;
}

main();
# Python - WRONG (can't await at top level before 3.10)
async def main():
    result = await fetch_data()
    return result

await main()  # SyntaxError in scripts before Python 3.10

# Correct - run with asyncio
import asyncio

async def main():
    result = await fetch_data()
    return result

if __name__ == '__main__':
    asyncio.run(main())  # Python 3.7+

# Or top-level await (3.10+ in scripts, 3.8+ in REPL)
await main()

Solution: Use asyncio.run() for entry point, or use top-level await in Python 3.10+ scripts.

8. JSON Serialization

Problem: Python's json module is stricter than JavaScript's JSON.stringify().

// TypeScript - serializes dates, undefined becomes null
JSON.stringify({
  date: new Date(),
  value: undefined
});  // {"date":"2024-...","value":null}
# Python - WRONG: TypeError for datetime
import json
from datetime import datetime

json.dumps({
    'date': datetime.now(),  # TypeError!
    'value': None
})

# Correct - custom encoder
class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime):
            return obj.isoformat()
        return super().default(obj)

json.dumps({'date': datetime.now()}, cls=CustomEncoder)

# Or convert before serialization
data = {
    'date': datetime.now().isoformat(),
    'value': None
}
json.dumps(data)

Solution: Use custom JSON encoders or convert objects before serialization. Libraries like pydantic handle this automatically.


Tooling

Tool Purpose Notes
mypy Static type checking Verifies type hints, like TypeScript compiler
pyright Static type checker Microsoft's type checker, VS Code integration
pydantic Runtime validation Validates data at runtime, like zod for TypeScript
black Code formatting Opinionated formatter, like prettier
ruff Linting & formatting Fast linter written in Rust, replaces flake8/black
pytest Testing framework Unit testing, like jest for TypeScript
hypothesis Property-based testing Generative testing, like fast-check
aiohttp Async HTTP client Like axios/fetch for async Python
httpx Sync/async HTTP Modern HTTP client supporting both modes
typer CLI framework Build CLIs with type hints, like commander
pydantic-settings Config management Type-safe config from env vars, like dotenv + zod
dataclasses Data structures Built-in, similar to TypeScript interfaces

Type Checking Setup

# pyproject.toml
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true

# pyrightconfig.json
{
  "typeCheckingMode": "strict",
  "reportMissingTypeStubs": true,
  "pythonVersion": "3.11"
}

Migration Tools

Tool Purpose Notes
2to3 Python 2 → 3 Built-in, not for TS→Python
js2py JavaScript → Python Transliterates JS, not recommended for TS
Manual TypeScript → Python No mature automated tool exists

Recommendation: Manual conversion following this skill. No production-ready TS→Python transpiler exists.


Examples

Example 1: Simple - HTTP Request Function

Before (TypeScript):

interface User {
  id: string;
  name: string;
  email: string;
}

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  return await response.json();
}

// Usage
try {
  const user = await fetchUser('123');
  console.log(user.name);
} catch (error) {
  console.error('Failed to fetch user:', error);
}

After (Python):

from dataclasses import dataclass
import httpx

@dataclass
class User:
    id: str
    name: str
    email: str

async def fetch_user(id: str) -> User:
    async with httpx.AsyncClient() as client:
        response = await client.get(f'https://api.example.com/users/{id}')

        if not response.is_success:
            raise Exception(f"HTTP {response.status_code}: {response.reason_phrase}")

        data = response.json()
        return User(**data)

# Usage
async def main():
    try:
        user = await fetch_user('123')
        print(user.name)
    except Exception as error:
        print(f'Failed to fetch user: {error}')

if __name__ == '__main__':
    import asyncio
    asyncio.run(main())

Example 2: Medium - Data Processing Pipeline

Before (TypeScript):

interface Product {
  id: string;
  name: string;
  price: number;
  category: string;
  inStock: boolean;
}

interface CategoryStats {
  category: string;
  count: number;
  averagePrice: number;
  totalValue: number;
}

function analyzeProducts(products: Product[]): CategoryStats[] {
  const grouped = products
    .filter(p => p.inStock)
    .reduce((acc, product) => {
      const category = product.category;
      if (!acc[category]) {
        acc[category] = [];
      }
      acc[category].push(product);
      return acc;
    }, {} as Record<string, Product[]>);

  return Object.entries(grouped).map(([category, items]) => {
    const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
    return {
      category,
      count: items.length,
      averagePrice: totalPrice / items.length,
      totalValue: totalPrice
    };
  }).sort((a, b) => b.totalValue - a.totalValue);
}

// Usage
const stats = analyzeProducts(products);
stats.forEach(s => {
  console.log(`${s.category}: ${s.count} items, avg $${s.averagePrice.toFixed(2)}`);
});

After (Python):

from dataclasses import dataclass
from typing import NamedTuple
from itertools import groupby
from operator import attrgetter

@dataclass
class Product:
    id: str
    name: str
    price: float
    category: str
    in_stock: bool

class CategoryStats(NamedTuple):
    category: str
    count: int
    average_price: float
    total_value: float

def analyze_products(products: list[Product]) -> list[CategoryStats]:
    # Filter in-stock products
    in_stock = [p for p in products if p.in_stock]

    # Group by category (requires sorted input)
    in_stock.sort(key=attrgetter('category'))
    grouped = groupby(in_stock, key=attrgetter('category'))

    # Calculate statistics
    stats = []
    for category, items in grouped:
        items_list = list(items)
        total_price = sum(item.price for item in items_list)
        count = len(items_list)

        stats.append(CategoryStats(
            category=category,
            count=count,
            average_price=total_price / count if count > 0 else 0,
            total_value=total_price
        ))

    # Sort by total value descending
    return sorted(stats, key=attrgetter('total_value'), reverse=True)

# Alternative: using defaultdict (more similar to TS reduce)
from collections import defaultdict

def analyze_products_alt(products: list[Product]) -> list[CategoryStats]:
    # Group by category
    grouped: dict[str, list[Product]] = defaultdict(list)
    for product in products:
        if product.in_stock:
            grouped[product.category].append(product)

    # Calculate statistics
    stats = []
    for category, items in grouped.items():
        total_price = sum(item.price for item in items)
        count = len(items)
        stats.append(CategoryStats(
            category=category,
            count=count,
            average_price=total_price / count,
            total_value=total_price
        ))

    return sorted(stats, key=lambda s: s.total_value, reverse=True)

# Usage
stats = analyze_products(products)
for s in stats:
    print(f"{s.category}: {s.count} items, avg ${s.average_price:.2f}")

Example 3: Complex - Generic Repository with Caching

Before (TypeScript):

interface Entity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends Entity {
  name: string;
  email: string;
}

interface CacheOptions {
  ttlSeconds: number;
  maxSize: number;
}

class CachedRepository<T extends Entity> {
  private cache = new Map<string, { value: T; expiresAt: number }>();
  private readonly ttl: number;
  private readonly maxSize: number;

  constructor(
    private readonly fetchFn: (id: string) => Promise<T>,
    options: CacheOptions = { ttlSeconds: 300, maxSize: 100 }
  ) {
    this.ttl = options.ttlSeconds * 1000;
    this.maxSize = options.maxSize;
  }

  async get(id: string): Promise<T | null> {
    // Check cache
    const cached = this.cache.get(id);
    if (cached && cached.expiresAt > Date.now()) {
      return cached.value;
    }

    // Fetch from source
    try {
      const value = await this.fetchFn(id);
      this.set(id, value);
      return value;
    } catch (error) {
      if (error.status === 404) {
        return null;
      }
      throw error;
    }
  }

  async getMany(ids: string[]): Promise<Map<string, T>> {
    const results = await Promise.all(
      ids.map(async id => ({ id, value: await this.get(id) }))
    );

    return new Map(
      results
        .filter(r => r.value !== null)
        .map(r => [r.id, r.value!])
    );
  }

  private set(id: string, value: T): void {
    // Evict oldest if at capacity
    if (this.cache.size >= this.maxSize) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }

    this.cache.set(id, {
      value,
      expiresAt: Date.now() + this.ttl
    });
  }

  invalidate(id: string): void {
    this.cache.delete(id);
  }

  clear(): void {
    this.cache.clear();
  }
}

// Usage
interface ApiError extends Error {
  status: number;
}

async function fetchUserFromApi(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    const error = new Error(`HTTP ${response.status}`) as ApiError;
    error.status = response.status;
    throw error;
  }
  return response.json();
}

const userRepo = new CachedRepository(fetchUserFromApi, {
  ttlSeconds: 600,
  maxSize: 50
});

// Fetch single user
const user = await userRepo.get('123');
if (user) {
  console.log(`Found user: ${user.name}`);
}

// Fetch multiple users
const users = await userRepo.getMany(['1', '2', '3']);
users.forEach((user, id) => {
  console.log(`${id}: ${user.name}`);
});

After (Python):

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Generic, TypeVar, Protocol, Callable, Awaitable
from collections import OrderedDict
import asyncio
import httpx

# Entity protocol
class Entity(Protocol):
    id: str
    created_at: datetime
    updated_at: datetime

# Concrete entity
@dataclass
class User:
    id: str
    name: str
    email: str
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)

T = TypeVar('T', bound=Entity)

@dataclass
class CacheOptions:
    ttl_seconds: int = 300
    max_size: int = 100

@dataclass
class CacheEntry(Generic[T]):
    value: T
    expires_at: datetime

class ApiError(Exception):
    def __init__(self, message: str, status: int):
        super().__init__(message)
        self.status = status

class CachedRepository(Generic[T]):
    def __init__(
        self,
        fetch_fn: Callable[[str], Awaitable[T]],
        options: CacheOptions | None = None
    ):
        self._fetch_fn = fetch_fn
        self._options = options or CacheOptions()
        self._cache: OrderedDict[str, CacheEntry[T]] = OrderedDict()
        self._ttl = timedelta(seconds=self._options.ttl_seconds)
        self._max_size = self._options.max_size

    async def get(self, id: str) -> T | None:
        # Check cache
        cached = self._cache.get(id)
        if cached and cached.expires_at > datetime.now():
            # Move to end (LRU)
            self._cache.move_to_end(id)
            return cached.value

        # Fetch from source
        try:
            value = await self._fetch_fn(id)
            self._set(id, value)
            return value
        except ApiError as error:
            if error.status == 404:
                return None
            raise

    async def get_many(self, ids: list[str]) -> dict[str, T]:
        # Fetch all in parallel
        results = await asyncio.gather(
            *[self._fetch_with_id(id) for id in ids],
            return_exceptions=False
        )

        # Filter out None values
        return {
            id: value
            for id, value in results
            if value is not None
        }

    async def _fetch_with_id(self, id: str) -> tuple[str, T | None]:
        value = await self.get(id)
        return (id, value)

    def _set(self, id: str, value: T) -> None:
        # Evict oldest if at capacity (FIFO eviction)
        if len(self._cache) >= self._max_size:
            self._cache.popitem(last=False)  # Remove oldest (first item)

        self._cache[id] = CacheEntry(
            value=value,
            expires_at=datetime.now() + self._ttl
        )

    def invalidate(self, id: str) -> None:
        self._cache.pop(id, None)

    def clear(self) -> None:
        self._cache.clear()

# Usage
async def fetch_user_from_api(id: str) -> User:
    async with httpx.AsyncClient() as client:
        response = await client.get(f'/api/users/{id}')

        if not response.is_success:
            raise ApiError(
                f"HTTP {response.status_code}",
                status=response.status_code
            )

        data = response.json()
        return User(
            id=data['id'],
            name=data['name'],
            email=data['email'],
            created_at=datetime.fromisoformat(data['createdAt']),
            updated_at=datetime.fromisoformat(data['updatedAt'])
        )

async def main():
    user_repo = CachedRepository(
        fetch_user_from_api,
        CacheOptions(ttl_seconds=600, max_size=50)
    )

    # Fetch single user
    user = await user_repo.get('123')
    if user:
        print(f"Found user: {user.name}")

    # Fetch multiple users
    users = await user_repo.get_many(['1', '2', '3'])
    for id, user in users.items():
        print(f"{id}: {user.name}")

    # Invalidate and clear
    user_repo.invalidate('123')
    user_repo.clear()

if __name__ == '__main__':
    asyncio.run(main())

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • convert-python-typescript - Reverse conversion (Python → TypeScript)
  • lang-typescript-dev - TypeScript development patterns
  • lang-python-dev - Python development patterns