There's a gap between 'code that runs' and 'code a team can maintain for five years.' Modern Python best practices close that gap ā not through exotic tricks, but through consistent, boring, well-supported conventions that every engineer on the team follows the same way.
1Formatting and Data Access: The Small Things That Compound
f-strings, introduced in 3.6 and continuously improved since, are not just shorter than %-formatting or .format() ā they're evaluated inline, which means your editor can syntax-highlight the expression inside {} and static analyzers can catch a typo in a variable name before you ever run the code. Since Python 3.12, f-strings also support reusing the same quote character inside the expression and can span multiple lines, removing two of the last remaining reasons to reach for .format().
pathlib.Path deserves the same treatment relative to manual string-based path handling. Concatenating paths with + or os.path.join string arguments is a well-known source of Windows/Unix bugs (\ vs /), and os.path functions all return plain strings with no attached behavior. Path objects support the / operator for joining, expose .exists(), .is_file(), .glob(), .read_text() and dozens of other methods directly, and are accepted almost everywhere a string path is expected in the standard library.
Neither change is dramatic on its own. What makes them 'best practices' rather than just preferences is that they eliminate entire categories of platform-specific and encoding-related bugs by construction, for the cost of a different import.
name = "Ada"
score = 97.456
# Modern
print(f"{name} scored {score:.1f}%")
# Avoid
print("%s scored %.1f%%" % (name, score))Ada scored 97.5%
2Types as Documentation, Not Bureaucracy
Adding -> float and price: float to a function signature costs almost nothing to write and pays back every time someone else ā including future you ā has to call that function without re-reading its body. Unlike a docstring, a type hint can't silently drift out of sync with the implementation without a type checker complaining, because tools like mypy and pyright actively verify it against how the function is used elsewhere in the codebase.
The common objection ā 'Python is dynamically typed, why fight it?' ā misunderstands what type hints do. They're opt-in, checked by external tools, and completely erased at runtime (with rare exceptions like dataclasses or pydantic that introspect them deliberately). You lose nothing of Python's flexibility; you gain a layer of verification that used to only exist in statically typed languages.
Professional teams typically require type hints on all public function signatures and class attributes, while allowing untyped code in test scripts or one-off internal helpers, and enforce this via mypy --strict (or pyright) in CI rather than manual code review.
from pathlib import Path
config_path = Path("config") / "settings.json"
if config_path.exists():
data = config_path.read_text()apply_discount(price: float, percent: float) -> float
3Composition, Pure Functions, and Why Structure Matters More Than Syntax
Deep inheritance hierarchies feel organized while you're building them and become brittle the moment requirements change, because a change to a base class ripples unpredictably through every subclass. Composition ā building behavior by combining small, focused objects and functions rather than inheriting it ā keeps that blast radius contained: a PaymentProcessor that *has* a Validator and a Logger is far easier to reason about than one that *is* a Validator that *is* a Logger through three layers of class.
Pure functions ā ones whose output depends only on their inputs, with no hidden reads or writes to global state, files, or network calls ā are the smallest unit of that philosophy. tax_for(income, rate) is trivial to unit test: call it, assert the result, done. A method buried inside a stateful class that reads self.config, mutates self.cache, and calls out to a database is not.
None of this means 'never use classes' ā dataclasses, protocols, and small well-scoped classes are exactly what later lessons in this module cover. The best practice is choosing the simplest structure that solves the problem: a function first, a small class second, an inheritance hierarchy only when there's a genuine is-a relationship that will stay stable.
def apply_discount(price: float, percent: float) -> float:
return price * (1 - percent / 100)10000.0 ā deterministic, no mocks needed
4Step-by-Step Breakdown
Two functions can do the exact same thing and still be worlds apart in quality. Let's look at what separates hobbyist Python from the code professional teams ship.
Rule one: use f-strings for formatting. They're faster than %-formatting and .format(), and since 3.12 they even support nested quotes and multi-line expressions.
Rule two: pathlib over string-concatenated os.path. Paths become objects with real methods, and they're cross-platform by construction.
Checkpoint: What is the main advantage of pathlib.Path over manual os.path string concatenation?
- āPaths are objects with methods, and are cross-platform by construction
- āIt always runs faster at the CPU level
Rule three: every public function gets a type hint. It's free documentation that your editor and mypy both understand.
Checkpoint: What is the main benefit of adding type hints to a public function?
- āIt documents intent and enables static analysis (mypy, editors)
- āPython enforces the types at runtime automatically
Rule four: prefer composition and small, pure functions over deep inheritance trees. A function with no side effects is trivially testable.
These conventions are the foundation everything else in this module builds on ā next, we'll formalize types with Type Hints.
Format a Real Score String. Finish format_score(): f-strings are the modern formatting standard.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Enforce style with tooling, not memory
Ruff, Black, and mypy running in pre-commit hooks and CI catch formatting and typing issues consistently, removing "did you remember the convention" from code review entirely.
Default to functions; reach for classes only when you need state or an interface
A module of well-named pure functions is usually easier to test and reason about than a class with several mutating methods, unless you genuinely need to bundle state and behavior together.
Frequent Bugs
Mixing os.path string paths and pathlib.Path objects across a codebase, causing subtle bugs when a function assumes one type but receives the other.
Standardize on pathlib.Path at all public function boundaries; call `Path(x)` at the edges if you must accept a plain string from user input or a legacy API.
Real-World Examples
Reading a Config Directory Cross-Platform
A CLI tool needs to locate and load all `.yaml` files in a config directory, correctly on both Windows and Linux CI runners.
from pathlib import Path
def load_config_files(config_dir: str) -> list[str]:
directory = Path(config_dir)
return [p.read_text(encoding="utf-8") for p in directory.glob("*.yaml")]