Python Standard Library Essentials: datetime, collections and json

AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.

Python 3.13

Python ships with a large standard library. This article covers the handful of modules you will import almost every day: time, specialized containers, JSON, environment access, subprocesses, randomness and file copying.

Environment

Examples are verified on Python 3.13 and use only built-in modules — nothing to install.

datetime: dates and times

from datetime import date, datetime, timedelta, timezone

today = date.today()                    # 2026-11-03
now = datetime.now()                    # local time, naive (no tzinfo)
utc = datetime.now(timezone.utc)        # aware

Arithmetic uses timedelta:

tomorrow = today + timedelta(days=1)
last_week = now - timedelta(weeks=1)
diff = datetime(2026, 12, 1) - now      # timedelta
diff.days                               # whole days between the two

Formatting and parsing:

now.strftime("%Y-%m-%d %H:%M:%S")       # '2026-11-03 14:30:00'
now.isoformat(timespec="seconds")       # '2026-11-03T14:30:00'

datetime.strptime("2026-11-03", "%Y-%m-%d")        # parse with an explicit format
datetime.fromisoformat("2026-11-03T14:30:00")      # parse ISO 8601

Time zones (use the built-in zoneinfo since 3.9, no pytz needed):

from zoneinfo import ZoneInfo

shanghai = datetime.now(ZoneInfo("Asia/Shanghai"))
utc_time = shanghai.astimezone(timezone.utc)       # convert, don't relabel

Unix timestamps:

now.timestamp()                         # seconds as float
datetime.fromtimestamp(1762000000, tz=timezone.utc)

collections: better containers

Counter

from collections import Counter

words = "a b a c a b".split()
counter = Counter(words)
counter["a"]              # 3
counter["zzz"]            # 0, no KeyError
counter.most_common(2)    # [('a', 3), ('b', 2)]
counter + Counter(["a"])  # element-wise addition
counter.update(["a", "d"])

defaultdict

from collections import defaultdict

groups = defaultdict(list)          # a missing key creates an empty list
for name, team in [("a", "red"), ("b", "blue"), ("c", "red")]:
    groups[team].append(name)
# {'red': ['a', 'c'], 'blue': ['b']}

Use defaultdict(int) for counting and defaultdict(dict) for nested structures. The factory takes no arguments; when you need a different default, pass lambda: 1.

deque

from collections import deque

queue = deque(["a", "b"])
queue.append("c")          # push right
queue.popleft()            # 'a' — O(1), unlike list.pop(0) which is O(n)
queue.appendleft("z")
queue.rotate(1)

history = deque(maxlen=3)  # bounded: oldest items fall off automatically

namedtuple

from collections import namedtuple

Row = namedtuple("Row", ["id", "name"])
r = Row(1, "a")
r.name          # 'a'
r._replace(id=2)

New code should prefer typing.NamedTuple, which supports annotations and defaults.

json: serialize and parse

import json

data = {"id": 1, "tags": ["py"], "active": True}
text = json.dumps(data, ensure_ascii=False, indent=2)
back = json.loads(text)                    # back == data

with open("config.json", encoding="utf-8") as f:
    config = json.load(f)

with open("config.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)
  • ensure_ascii=False keeps non-ASCII text readable instead of \uXXXX escapes.
  • json.loads raises json.JSONDecodeError on malformed input — catch it at the boundary where external data enters.
  • Types map to JSON primitives: dict → object, list/tuple → array, None → null. datetime and dataclasses are not serializable by default; add a default= function (dataclasses.asdict helps).
  • JSON has no comments and no trailing commas; a config file that works in Python will not parse as JSON.
  • Duplicate keys are silently collapsed to the last occurrence — do not rely on order or duplicates.

os and sys: environment and process

import os, sys

os.getenv("API_KEY")                 # None when unset
os.getenv("PORT", "8000")            # with a default
os.environ["API_KEY"] = "..."        # affects only this process and its children

sys.argv                             # ['script.py', '--flag']; argv[0] is the script
sys.version_info                     # named tuple, comparable: sys.version_info >= (3, 11)
sys.exit(1)                          # exit code 1

Anything os.path does, pathlib does better in new code — see the file I/O article.

subprocess: run external commands

import subprocess

result = subprocess.run(
    ["git", "rev-parse", "HEAD"],
    capture_output=True, text=True, check=True,
)
result.stdout.strip()      # commit hash
  • Pass arguments as a list. Avoid shell=True with interpolated strings, which invites command injection.
  • check=True turns a non-zero exit code into subprocess.CalledProcessError (with .returncode and .stderr).
  • text=True decodes output to str; otherwise you get bytes.

random and secrets

import random, secrets

random.choice(["a", "b"])            # one random element
random.sample(range(100), 5)         # 5 distinct values
random.randint(1, 10)                # inclusive on both ends

secrets.token_urlsafe(32)            # cryptographically secure token
secrets.choice(["a", "b"])

random is a Mersenne Twister: fast, reproducible, and predictable. Anything that guards access — tokens, passwords, session IDs, password-reset links — must come from secrets.

shutil: copying and archives

import shutil

shutil.copy2("a.txt", "backup/a.txt")     # copy contents plus metadata
shutil.move("a.txt", "archive/")
shutil.rmtree("tmp/")                     # recursive delete — no undo
shutil.make_archive("dist", "zip", "build/")

Common mistakes

1. Mixing naive and aware datetimes

Subtracting a naive datetime.now() from an aware datetime.now(timezone.utc) raises TypeError: can't subtract offset-naive and offset-aware datetimes. Pick one convention: store UTC aware values internally, convert to local time only for display.

2. Doing date math with time.time()

Seconds since the epoch ignore months, leap years and time zones. Use datetime plus timedelta.

3. Reading a defaultdict as if it were a dict

Accessing a missing key inserts it, so if key in d flips to True after the first read, and len(d) grows. Use d.get(key, default) when you only want to look.

4. Building shell strings for subprocess

subprocess.run(f"cat {filename}", shell=True)   # breaks on spaces and metacharacters

Use ["cat", filename] instead; it also avoids quoting bugs entirely.

5. Generating tokens with random

random is seeded predictably and its output can be reconstructed from enough samples. Use secrets.

6. Treating json.load as validation

Parsing only proves the text is syntactically valid JSON. Check required keys and types yourself, or use a schema library, before trusting the data.

Summary

  • Time: datetime + timedelta, zoneinfo for zones, store UTC aware values.
  • Counting: Counter; grouping: defaultdict; queues and sliding windows: deque.
  • JSON: dumps/loads with ensure_ascii=False; decode errors surface as JSONDecodeError.
  • Config from os.getenv, argv from sys.argv (use argparse for anything non-trivial).
  • External commands: subprocess.run([...], check=True); tokens: secrets.

Next: regular expressions, the last tool in this intermediate set.