Idiomatic Python patterns and best practices for building robust, efficient, and maintainable applications.
→ PEP 8 Style Guide · The Zen of Python · Python docs
Readability Counts. Python prioritizes readability. Code should be obvious and easy to understand.
# Good: Clear and readable
def get_active_users(users: list[User]) -> list[User]:
"""Return only active users from the provided list."""
return [user for user in users if user.is_active]
# Bad: Clever but confusing
def get_active_users(u):
return [x for x in u if x.a]→ typing module · PEP 484 · see python-typing.md for a deep dive
from typing import Any
def process_user(
user_id: str,
data: dict[str, Any],
active: bool = True
) -> User | None:
"""Process a user and return the updated User or None."""
if not active:
return None
return User(user_id, data)Use built-in types directly as generics, and | for unions — no import from typing needed. This is the default to reach for; only fall back to typing.List/Dict/Optional when the code must also run on Python 3.8 or earlier.
# Python 3.9+ — use built-in types
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Python 3.8 and earlier — import from typing
from typing import List, Dict
def process_items(items: List[str]) -> Dict[str, int]:
return {item: len(item) for item in items}from typing import TypeVar, Union
# Type alias for complex types
JSON = Union[dict[str, Any], list[Any], str, int, float, bool, None]
def parse_json(data: str) -> JSON:
return json.loads(data)
# Generic types
T = TypeVar('T')
def first(items: list[T]) -> T | None:
"""Return the first item or None if list is empty."""
return items[0] if items else Nonefrom typing import Protocol
class Renderable(Protocol):
def render(self) -> str:
"""Render the object to a string."""
def render_all(items: list[Renderable]) -> str:
"""Render all items that implement the Renderable protocol."""
return "\n".join(item.render() for item in items)→ Exceptions tutorial · Built-in exceptions
# Good: Catch specific exceptions
def load_config(path: str) -> Config:
try:
with open(path) as f:
return Config.from_json(f.read())
except FileNotFoundError as e:
raise ConfigError(f"Config file not found: {path}") from e
except json.JSONDecodeError as e:
raise ConfigError(f"Invalid JSON in config: {path}") from e
# Bad: Bare except
def load_config(path: str) -> Config:
try:
with open(path) as f:
return Config.from_json(f.read())
except:
return None # Silent failure!def process_data(data: str) -> Result:
try:
parsed = json.loads(data)
except json.JSONDecodeError as e:
# Chain exceptions to preserve the traceback
raise ValueError(f"Failed to parse data: {data}") from eclass AppError(Exception):
"""Base exception for all application errors."""
pass
class ValidationError(AppError):
"""Raised when input validation fails."""
pass
class NotFoundError(AppError):
"""Raised when a requested resource is not found."""
pass
# Usage
def get_user(user_id: str) -> User:
user = db.find_user(user_id)
if not user:
raise NotFoundError(f"User not found: {user_id}")
return user→ contextlib · Context Manager Protocol
# Good: Using context managers
def process_file(path: str) -> str:
with open(path, 'r') as f:
return f.read()
# Bad: Manual resource management
def process_file(path: str) -> str:
f = open(path, 'r')
try:
return f.read()
finally:
f.close()from contextlib import contextmanager
@contextmanager
def timer(name: str):
"""Context manager to time a block of code."""
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f"{name} took {elapsed:.4f} seconds")
# Usage
with timer("data processing"):
process_large_dataset()class DatabaseTransaction:
def __init__(self, connection):
self.connection = connection
def __enter__(self):
self.connection.begin_transaction()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.connection.commit()
else:
self.connection.rollback()
return False # Don't suppress exceptions
# Usage
with DatabaseTransaction(conn):
user = conn.create_user(user_data)
conn.create_profile(user.id, profile_data)→ Generator expressions · List/set/dict displays
# Good: List comprehension for simple transformations
names = [user.name for user in users if user.is_active]
# Bad: Manual loop
names = []
for user in users:
if user.is_active:
names.append(user.name)
# Chained filters are just conditions to merge with `and` — still one readable comprehension
# Bad: unnecessary second `if` clause
result = [x * 2 for x in items if x > 0 if x % 2 == 0]
# Good: merge the conditions, no need to regress to a manual loop
result = [x * 2 for x in items if x > 0 and x % 2 == 0]
# Reach for a full function only once there's genuine multi-step logic,
# not just an extra filter:
def filter_and_transform(items: Iterable[int]) -> list[int]:
result = []
for x in items:
if x > 0 and x % 2 == 0:
transformed = expensive_multi_step_transform(x)
if transformed is not None:
result.append(transformed)
return result# Good: Generator for lazy evaluation
total = sum(x * x for x in range(1_000_000))
# Bad: Creates large intermediate list
total = sum([x * x for x in range(1_000_000)])def read_large_file(path: str) -> Iterator[str]:
"""Read a large file line by line."""
with open(path) as f:
for line in f:
yield line.strip()
# Usage
for line in read_large_file("huge.txt"):
process(line)→ dataclasses · collections.namedtuple
Dataclasses are a great alternative to dictionaries or regular classes for simple data containers. They automatically generate __init__, __repr__, and __eq__ methods, and are strict about their attributes (typing is easier than using TypedDict).
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class User:
"""User entity with automatic __init__, __repr__, and __eq__."""
id: str
name: str
email: str
created_at: datetime = field(default_factory=datetime.now)
is_active: bool = True
def __post_init__(self):
"""Validate fields right after __init__ runs."""
if "@" not in self.email:
raise ValueError(f"Invalid email: {self.email}")
# Usage
user = User(id="123", name="Alice", email="alice@example.com")from dataclasses import dataclass, field, InitVar
from typing import ClassVar
@dataclass
class User:
# ClassVar — class-level attribute, excluded from __init__ and __repr__
registry: ClassVar[dict[str, "User"]] = {}
id: str
name: str
tags: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
User.registry[self.id] = self
# InitVar — passed to __post_init__ but NOT stored as a field on the instance
@dataclass
class Connection:
host: str
port: int
password: InitVar[str] # available in __post_init__, then discarded
def __post_init__(self, password: str) -> None:
self._client = connect(self.host, self.port, password)A NamedTuple is more memory-efficient than a dataclass because it stores data exactly like a tuple. It is immutable and hashable by default. Use a dataclass when you need mutability.
from typing import NamedTuple
class Point(NamedTuple):
"""Immutable 2D point."""
x: float
y: float
def distance(self, other: 'Point') -> float:
return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5
p1 = Point(0, 0)
p2 = Point(3, 4)
print(p1.distance(p2)) # 5.0→ PEP 318 · functools.wraps · ParamSpec
Decorators modify the behavior of functions or classes without changing their source. Always use functools.wraps to preserve the original function's metadata (name, docstring, signature).
import functools
import time
def timer(func: Callable) -> Callable:
"""Decorator to time function execution."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
# slow_function() prints: slow_function took 1.0012sWhen a decorator needs arguments, add one extra level of nesting: a factory that returns the actual decorator.
def retry(max_attempts: int = 3, exceptions: tuple = (Exception,)):
"""Decorator factory — called with arguments, returns the actual decorator."""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == max_attempts - 1:
raise
return wrapper
return decorator
@retry(max_attempts=5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url: str) -> str: ...Without ParamSpec, a decorator's wrapper has type (*args: Any, **kwargs: Any) -> R, losing all parameter names and types from the decorated function. ParamSpec preserves the full signature.
from typing import ParamSpec, TypeVar
from collections.abc import Callable
P = ParamSpec("P")
R = TypeVar("R")
def retry(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
for attempt in range(3):
try:
return func(*args, **kwargs)
except Exception:
if attempt == 2:
raise
raise RuntimeError("unreachable")
return wrapper
@retry
def fetch(url: str, timeout: int = 30) -> str: ...
fetch("https://example.com", timeout=10) # type checker knows both args and return type
fetch(timeout=10) # type error — url is required→ concurrent.futures · asyncio · threading
import concurrent.futures
def fetch_url(url: str) -> str:
"""Fetch a URL (I/O-bound operation)."""
import urllib.request
with urllib.request.urlopen(url) as response:
return response.read().decode()
def fetch_all_urls(urls: list[str]) -> dict[str, str]:
"""Fetch multiple URLs concurrently using threads."""
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
future_to_url = {executor.submit(fetch_url, url): url for url in urls}
results = {}
for future in concurrent.futures.as_completed(future_to_url):
url = future_to_url[future]
try:
results[url] = future.result()
except Exception as e:
results[url] = f"Error: {e}"
return resultsdef process_data(data: list[int]) -> int:
"""CPU-intensive computation."""
return sum(x ** 2 for x in data)
def process_all(datasets: list[list[int]]) -> list[int]:
"""Process multiple datasets using multiple processes."""
with concurrent.futures.ProcessPoolExecutor() as executor:
results = list(executor.map(process_data, datasets))
return resultsimport asyncio
async def fetch_async(url: str) -> str:
"""Fetch a URL asynchronously."""
import aiohttp
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def fetch_all(urls: list[str]) -> dict[str, str]:
"""Fetch multiple URLs concurrently."""
tasks = [fetch_async(url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return dict(zip(urls, results))→ __slots__ · Generator expressions
# Bad: Regular class uses __dict__ (more memory)
class Point:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# Good: __slots__ reduces memory usage
class Point:
__slots__ = ['x', 'y']
def __init__(self, x: float, y: float):
self.x = x
self.y = y# Bad: Returns full list in memory
def read_lines(path: str) -> list[str]:
with open(path) as f:
return [line.strip() for line in f]
# Good: Yields lines one at a time
def read_lines(path: str) -> Iterator[str]:
with open(path) as f:
for line in f:
yield line.strip()→ property
Properties let you add validation or computation to attribute access without changing the public interface — the Pythonic alternative to Java-style getX()/setX() methods.
class Temperature:
def __init__(self, celsius: float = 0.0):
self._celsius = celsius # _ prefix signals "private by convention"
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError(f"Temperature below absolute zero: {value}")
self._celsius = value
@property
def fahrenheit(self) -> float:
"""Computed, read-only property — no setter defined."""
return self._celsius * 9 / 5 + 32
t = Temperature(100)
print(t.fahrenheit) # 212.0
t.celsius = -300 # raises ValueErrorclass User:
def __init__(self, name: str, email: str):
self.name = name
self.email = email
@classmethod
def from_dict(cls, data: dict) -> "User":
"""Alternative constructor — cls is the class itself, so subclasses
that inherit this factory will instantiate themselves, not User."""
return cls(name=data["name"], email=data["email"])
@staticmethod
def validate_email(email: str) -> bool:
"""Pure utility — no access to cls or self needed.
Belongs logically to User but is completely stateless."""
return "@" in email and "." in email.split("@")[-1]
user = User.from_dict({"name": "Alice", "email": "alice@example.com"})
User.validate_email("bad-email") # FalseRule of thumb: use @classmethod when the method needs access to the class itself (factory patterns, class-level registries), use @staticmethod for stateless utilities that need neither self nor cls.
Dunder methods let your objects integrate seamlessly with Python's syntax and built-ins. The language calls them implicitly — you never call obj.__len__() directly, you call len(obj).
class Vector:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
def __repr__(self) -> str:
"""Unambiguous — shown in REPL, logs, and debuggers.
Goal: eval(repr(obj)) == obj when practical."""
return f"Vector({self.x!r}, {self.y!r})"
def __str__(self) -> str:
"""Human-readable — shown by print() and str().
Falls back to __repr__ when not defined."""
return f"({self.x}, {self.y})" def __add__(self, other: "Vector") -> "Vector":
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, scalar: float) -> "Vector":
return Vector(self.x * scalar, self.y * scalar)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Vector):
return NotImplemented # Not False — lets Python try the reflected operation
return self.x == other.x and self.y == other.y
def __hash__(self) -> int:
# Defining __eq__ without __hash__ makes the object unhashable (can't be in a set or dict key).
# Rule: if two objects are equal, they must have the same hash.
return hash((self.x, self.y))
def __bool__(self) -> bool:
"""Truthiness — used by if obj:, while obj:, and bool(obj)."""
return self.x != 0 or self.y != 0class Grid:
def __init__(self, rows: int, cols: int):
self._data = [[0] * cols for _ in range(rows)]
def __len__(self) -> int:
return len(self._data)
def __getitem__(self, index: int) -> list[int]:
return self._data[index]
def __setitem__(self, index: int, value: list[int]) -> None:
self._data[index] = value
def __contains__(self, item: int) -> bool:
return any(item in row for row in self._data)class CountDown:
def __init__(self, start: int):
self.current = start
def __iter__(self) -> "CountDown":
return self # The object is its own iterator
def __next__(self) -> int:
if self.current <= 0:
raise StopIteration
value = self.current
self.current -= 1
return value
for n in CountDown(3):
print(n) # 3, 2, 1class Multiplier:
def __init__(self, factor: float):
self.factor = factor
def __call__(self, value: float) -> float:
return value * self.factor
double = Multiplier(2)
double(5) # 10 — called just like a function
list(map(double, [1, 2, 3])) # [2, 4, 6]→ enum
Enums replace magic strings and integers with named, type-safe constants. They prevent typos, enable exhaustive checking, and make intent explicit.
from enum import Enum, IntEnum, Flag, auto
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
# Enum members are singletons — compare with is or ==
Color.RED is Color.RED # True
Color.RED == 1 # False — Enum members are not plain ints
# Access by value or name
Color(1) # Color.RED
Color["RED"] # Color.RED
Color.RED.name # "RED"
Color.RED.value # 1
# Iteration
for color in Color:
print(color.name, color.value)
# IntEnum — when you need integer comparison (e.g. HTTP status codes, priority queues)
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
Priority.HIGH > Priority.LOW # True
# auto() — values assigned automatically
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
# Flag — for bitwise combination of options
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
ALL = READ | WRITE | EXECUTE
user_perms = Permission.READ | Permission.WRITE
Permission.READ in user_perms # True
Permission.EXECUTE in user_perms # False→ abc · collections.abc
ABCs enforce that subclasses implement a specific interface. The contract is explicit at class definition time rather than silently failing at call time.
from abc import ABC, abstractmethod
import math
class Shape(ABC):
"""Cannot be instantiated — exists only to define the interface."""
@abstractmethod
def area(self) -> float: ...
@abstractmethod
def perimeter(self) -> float: ...
def describe(self) -> str:
"""Concrete methods are fine — shared by all subclasses."""
return f"Area: {self.area():.2f}, Perimeter: {self.perimeter():.2f}"
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self.radius
Shape() # TypeError: Can't instantiate abstract class Shape
circle = Circle(5)
circle.describe() # "Area: 78.54, Perimeter: 31.42"ABC vs Protocol: ABC enforces inheritance — subclasses must explicitly inherit from the ABC. Protocol uses structural subtyping (duck typing) — any class with the right methods satisfies it regardless of inheritance. Use Protocol for external or third-party types you cannot control; use ABC for your own class hierarchy where you want an enforced contract.
The collections module provides specialized container types that solve common patterns more cleanly — and often more efficiently — than plain dicts and lists.
from collections import defaultdict, Counter, deque, ChainMap
# defaultdict — initializes missing keys automatically, avoiding KeyError
word_groups: defaultdict[int, list[str]] = defaultdict(list)
for word in ["apple", "banana", "cat", "dog"]:
word_groups[len(word)].append(word)
# {5: ['apple'], 6: ['banana'], 3: ['cat', 'dog']}
graph: defaultdict[str, list[str]] = defaultdict(list)
graph["a"].append("b") # No KeyError even for unseen keys
# Counter — count occurrences and tally
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
counts.most_common(2) # [('apple', 3), ('banana', 2)]
# Counter supports arithmetic
a = Counter(["x", "y", "x"])
b = Counter(["x", "z"])
a + b # Counter({'x': 3, 'y': 1, 'z': 1})
a - b # Counter({'y': 1, 'x': 1})
# deque — O(1) appends and pops on both ends
# list.insert(0, ...) and list.pop(0) are O(n) — deque fixes that
buffer: deque[int] = deque(maxlen=3) # fixed-size sliding window
for i in range(5):
buffer.append(i)
# deque([2, 3, 4], maxlen=3) — oldest element auto-evicted
buffer.appendleft(0) # O(1)
buffer.popleft() # O(1)
# ChainMap — overlay multiple dicts, searched left-to-right
defaults = {"color": "red", "timeout": 30}
overrides = {"color": "blue"}
config = ChainMap(overrides, defaults)
config["color"] # "blue" — from overrides
config["timeout"] # 30 — fallback to defaultsCache the return value of a function keyed on its arguments. Subsequent calls with the same arguments return the cached result without re-executing the function.
import functools
# @cache — unbounded cache (Python 3.9+), simpler than lru_cache
@functools.cache
def fibonacci(n: int) -> int:
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# @lru_cache — bounded with LRU eviction (use when memory is a concern)
@functools.lru_cache(maxsize=128)
def expensive_query(user_id: str) -> dict:
return db.fetch_user(user_id) # Only hits the database on cache miss
expensive_query.cache_info() # CacheInfo(hits=..., misses=..., maxsize=128, currsize=...)
expensive_query.cache_clear() # Invalidate the entire cache
# Cached functions require hashable arguments — lists are not hashable
# Bad
@functools.cache
def process(items: list[int]) -> int: ...
# Good — use tuples
@functools.cache
def process(items: tuple[int, ...]) -> int: ...from functools import partial
def power(base: int, exponent: int) -> int:
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
square(5) # 25
cube(3) # 27→ itertools · Itertools Recipes
itertools provides building blocks for lazy, memory-efficient iteration. Compose these instead of writing manual loops over intermediate lists.
import itertools
# chain — flatten multiple iterables into one stream
list(itertools.chain([1, 2], [3, 4], [5])) # [1, 2, 3, 4, 5]
list(itertools.chain.from_iterable([[1, 2], [3, 4]])) # same, for a list-of-lists
# cycle — infinite repetition
spinner = itertools.cycle(["|", "/", "-", "\\"])
next(spinner) # "|", then "/", then "-", then "\\" ...
# batched — chunk into fixed-size groups (Python 3.12+)
list(itertools.batched(range(10), 3)) # [(0,1,2), (3,4,5), (6,7,8), (9,)]The module also provides: islice (lazy slice), groupby (group consecutive elements — sort first), product (Cartesian product), combinations / permutations, takewhile / dropwhile, accumulate, and more. See the official docs and the recipes section for the full reference.
→ pathlib
pathlib.Path replaces os.path with an object-oriented, readable API. Prefer it for all filesystem operations.
from pathlib import Path
# Construction — / operator joins path segments (cross-platform)
config = Path.home() / ".config" / "app" / "settings.json"
data_dir = Path("/var/data")
# Reading and writing
text = config.read_text(encoding="utf-8")
config.write_text('{"debug": false}', encoding="utf-8")
raw = config.read_bytes()
config.write_bytes(b"\x00\x01")
# Inspection
config.exists() # True / False
config.is_file() # True
config.is_dir() # False
# Navigation
config.parent # Path('/home/user/.config/app')
config.name # 'settings.json'
config.stem # 'settings'
config.suffix # '.json'
config.suffixes # ['.tar', '.gz'] for 'archive.tar.gz'
# Listing and globbing
for child in data_dir.iterdir():
print(child)
for py_file in Path("src").rglob("*.py"): # recursive
process(py_file)
# Creating directories
Path("output/cache").mkdir(parents=True, exist_ok=True)
# Renaming and deleting
config.rename(config.with_suffix(".bak"))
config.unlink(missing_ok=True) # delete; no error if already missingAlways prefer pathlib.Path over os.path — it composes cleanly, works cross-platform, and eliminates string manipulation for path operations.
→ PEP 3102 (keyword-only) · PEP 570 (positional-only)
Python lets you be explicit about how arguments must be passed at the call site.
# Keyword-only — parameters after * must be named by the caller
def create_user(name: str, *, email: str, active: bool = True) -> User:
...
create_user("Alice", email="alice@example.com") # OK
create_user("Alice", "alice@example.com") # TypeError
# Positional-only — parameters before / cannot be named by the caller
def distance(x: float, y: float, /) -> float:
return (x ** 2 + y ** 2) ** 0.5
distance(3, 4) # OK
distance(x=3, y=4) # TypeError
# Combined: positional-only / normal * keyword-only
def move(x: float, y: float, /, speed: float = 1.0, *, animate: bool = False) -> None:
"""x, y: positional only. speed: either. animate: keyword only."""
...When to use each:
- Positional-only (
/): parameter names are implementation details that you may rename without breaking callers. Common in math and C-extension functions wherex,y,nare uninformative names. - Keyword-only (
*): prevents subtle argument-order bugs, especially with booleans and flags.create_file("path", True, False)is ambiguous;create_file("path", overwrite=True, binary=False)is not.
__all__ is a list of names that defines the public interface of a module.
# mymodule.py
__all__ = ["PublicClass", "public_function"]
class PublicClass:
"""Part of the public API."""
...
class _PrivateHelper:
"""Internal — excluded from __all__, not exported."""
...
def public_function() -> None:
...
def _internal() -> None:
...Two effects:
from mymodule import *imports only names in__all__- Documents intent — IDEs,
help(), and documentation generators surface__all__as the public API
Even when you never use import *, defining __all__ is good practice as a clear declaration of intent about what is stable and what is internal.
# Bad: Mutable default arguments
def append_to(item, items=[]):
items.append(item)
return items
# Good: Use None and create new list
def append_to(item, items=None):
if items is None:
items = []
items.append(item)
return items
#=========
# Bad: Checking type with type()
if type(obj) == list:
process(obj)
# Good: Use isinstance
if isinstance(obj, list):
process(obj)
#==========
# Bad: Comparing to None with ==
if value == None:
process()
# Good: Use is
if value is None:
process()
#==========
# Bad: from module import *
from os.path import *
# Good: Explicit imports
from os.path import join, exists
#==========
# Bad: Bare except
try:
risky_operation()
except:
pass
# Good: Specific exception
try:
risky_operation()
except SpecificError as e:
logger.error(f"Operation failed: {e}")
#==========
# Avoid String Concatenation in Loops
# Bad: O(n²) due to string immutability
result = ""
for item in items:
result += str(item)
# Good: O(n) using join
result = "".join(str(item) for item in items)
# Good: Using StringIO for building
from io import StringIO
buffer = StringIO()
for item in items:
buffer.write(str(item))
result = buffer.getvalue()Remember: Python code should be readable, explicit, and follow the principle of least surprise. When in doubt, prioritize clarity over cleverness.