Codetail

Article 8 of 8

Standard Library You Should Already Be Using

pathlib, zoneinfo, tomllib, cached_property.

22 min read

pathlib: stop using os.path

os.path works with strings. pathlib.Path works with objects. The difference matters because string paths compose poorly, while Path objects carry their operations with them.

Python
1import os
2
3# os.path: strings all the way, function soup
4base = "/var/log"
5app_dir = os.path.join(base, "myapp")
6log_file = os.path.join(app_dir, "app.log")
7stem = os.path.splitext(os.path.basename(log_file))[0]
8parent = os.path.dirname(log_file)
9
10print(log_file) # /var/log/myapp/app.log
11print(stem) # app
12print(parent) # /var/log/myapp
Python
1from pathlib import Path
2
3# pathlib: object-oriented, / operator for joining
4base = Path("/var/log")
5log_file = base / "myapp" / "app.log"
6
7print(log_file) # /var/log/myapp/app.log
8print(log_file.stem) # app
9print(log_file.suffix) # .log
10print(log_file.parent) # /var/log/myapp
11print(log_file.name) # app.log
12
13# Read/write directly on Path objects
14# log_file.write_text("hello")
15# contents = log_file.read_text()
16
17# Glob and rglob
18# for py_file in Path("src").rglob("*.py"):
19# print(py_file)

Path.walk() (3.12): os.walk replacement

Python
1from pathlib import Path
2
3# Before 3.12: os.walk returns string tuples
4import os
5for dirpath, dirnames, filenames in os.walk("/tmp/project"):
6 for filename in filenames:
7 full = os.path.join(dirpath, filename) # back to string hell
8
9# Python 3.12: Path.walk() returns Path objects
10for dirpath, dirnames, filenames in Path("/tmp/project").walk():
11 for filename in filenames:
12 full = dirpath / filename # Path all the way through

zoneinfo: timezones without pytz (3.9)

For years, handling timezones in Python meant installing pytz and learning its quirks (always use localize(), never pass a pytz timezone to replace()). Python 3.9 ships zoneinfo in the standard library, backed by the system timezone database.

Python
1# Before 3.9: had to install pytz
2import pytz
3from datetime import datetime
4
5tz = pytz.timezone("America/New_York")
6now = datetime.now(tz) # OK
7also_now = datetime(2024, 1, 1, tzinfo=tz) # Wrong! Must use localize()
8
9# The correct way with pytz:
10also_now = tz.localize(datetime(2024, 1, 1)) # confusing API
Python
1from datetime import datetime
2from zoneinfo import ZoneInfo
3
4# Python 3.9: works like a normal tzinfo -- no special API
5now = datetime.now(ZoneInfo("America/New_York"))
6print(now)
7
8# Creating aware datetimes
9meeting = datetime(2026, 3, 15, 14, 30, tzinfo=ZoneInfo("Europe/London"))
10print(meeting)
11
12# Converting between timezones
13in_new_york = meeting.astimezone(ZoneInfo("America/New_York"))
14print(in_new_york)
15
16# UTC is always available
17from datetime import timezone
18utc_now = datetime.now(timezone.utc)
19print(utc_now)

On some Linux systems without a system timezone database, you need to pip install tzdata as a fallback. This is the only external package needed and it is a pure-data package. On macOS and Windows, the system database is always available.

tomllib: built-in TOML parsing (3.11)

TOML is the config format used by pyproject.toml and Cargo, Rust's package manager. Before 3.11, reading it required a third-party package (usually tomli or toml). Python 3.11 ships tomllib in the standard library.

Python
1# config.toml
2# [server]
3# host = "localhost"
4# port = 8080
5# debug = false
6#
7# [database]
8# url = "postgresql://localhost/mydb"
9# pool_size = 10
10# timeout = 30.0
11#
12# [features]
13# enabled = ["auth", "logging", "metrics"]
Python
1import tomllib
2
3# Must open in binary mode ("rb")
4with open("config.toml", "rb") as f:
5 config = tomllib.load(f)
6
7print(config["server"]["host"]) # localhost
8print(config["server"]["port"]) # 8080
9print(config["database"]["pool_size"]) # 10
10print(config["features"]["enabled"]) # ['auth', 'logging', 'metrics']
11
12# Parse from string (bytes)
13toml_str = b"""
14[app]
15name = "codetail"
16version = "1.0.0"
17"""
18data = tomllib.loads(toml_str.decode())
19print(data["app"]["name"]) # codetail

Note: tomllib is read-only. There is no built-in writer. For writing TOML, you still need tomli-w or tomllib's companion package. For most use cases (reading config files), the standard library is all you need.

functools: partial, reduce, and total_ordering

Beyond @cache, the functools module has several utilities that remove boilerplate from common patterns.

partial: pre-fill function arguments

Python
1from functools import partial
2
3def power(base: float, exponent: float) -> float:
4 return base ** exponent
5
6# Before: write a one-liner wrapper every time
7def square(n: float) -> float:
8 return power(n, 2)
9
10# After: partial freezes some arguments
11square = partial(power, exponent=2)
12cube = partial(power, exponent=3)
13
14print(square(4)) # 16.0
15print(cube(3)) # 27.0
16
17# Practical: pre-configure a logging function
18import logging
19logging.basicConfig(level=logging.DEBUG)
20
21debug = partial(logging.log, logging.DEBUG)
22debug("Server started on port %d", 8080)

total_ordering: define two methods, get six

Python
1from functools import total_ordering
2
3# Without total_ordering: must implement __lt__, __le__, __gt__, __ge__, __eq__
4# With total_ordering: define __eq__ and one of the ordering methods
5
6@total_ordering
7class Version:
8 def __init__(self, major: int, minor: int, patch: int) -> None:
9 self.major = major
10 self.minor = minor
11 self.patch = patch
12
13 def __eq__(self, other: object) -> bool:
14 if not isinstance(other, Version):
15 return NotImplemented
16 return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
17
18 def __lt__(self, other: "Version") -> bool:
19 return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
20
21v1 = Version(1, 2, 3)
22v2 = Version(1, 3, 0)
23
24print(v1 < v2) # True -- from __lt__
25print(v1 > v2) # False -- generated by total_ordering
26print(v1 <= v2) # True -- generated
27print(sorted([v2, v1])) # [Version(1,2,3), Version(1,3,0)]

One last note: if ordering is the primary reason you are writing a class, @dataclass(order=True) generates all six comparison methods automatically from the field order. Reserve @total_ordering for classes with custom comparison logic.