# Python Practical Idioms Cheat Sheet

## Sorting

```python
nums.sort()                              # in place, returns None!
sorted(nums)                             # new list, works on any iterable
sorted(nums, reverse=True)

sorted(words, key=len)                   # by length
sorted(words, key=str.lower)             # case-insensitive
sorted(users, key=lambda u: u.age)       # by attribute
sorted(users, key=lambda u: (u.dept, -u.salary))   # multi-key: dept asc, salary desc

from operator import itemgetter, attrgetter
sorted(rows, key=itemgetter(2))          # by column 2
sorted(rows, key=itemgetter("date"))     # list of dicts
sorted(users, key=attrgetter("name"))

# Descending on a non-numeric secondary key: sort twice (Timsort is stable)
data.sort(key=itemgetter("name"))            # secondary first
data.sort(key=itemgetter("dept"), reverse=True)  # primary last

max(nums), min(words, key=len)           # min/max take key= too
sorted(d.items(), key=itemgetter(1), reverse=True)   # dict by value

import bisect                            # keep a list sorted / binary search
i = bisect.bisect_left(sorted_list, x)   # insertion point (== index if present)
bisect.insort(sorted_list, x)

import heapq                             # top-k without full sort: O(n log k)
heapq.nlargest(3, nums)
heapq.nsmallest(3, users, key=lambda u: u.age)
```

## Lists

```python
lst[-1], lst[-2]                         # last, second to last
lst[2:5], lst[:3], lst[3:], lst[::2]     # slices (copy)
lst[::-1]                                # reversed copy
lst[:]  /  lst.copy()                    # shallow copy
lst[1:3] = [10, 20, 30]                  # splice: replace a slice

lst.append(x)        # one element         O(1)
lst.extend(iter)     # many elements       lst += iter
lst.insert(0, x)     # O(n) — use deque for queues
lst.pop()            # last, O(1)
lst.pop(0)           # first, O(n) — again: deque
lst.remove(x)        # first occurrence by VALUE, ValueError if absent
del lst[i]           # by index

[x * 2 for x in nums if x > 0]           # filter + map in one
[a if a else "-" for a in cells]         # ternary inside: transform all
[y for row in grid for y in row]         # flatten one level (left-to-right nesting)

list(dict.fromkeys(items))               # dedupe KEEPING order
list(set(items))                         # dedupe, order lost

", ".join(str(x) for x in nums)          # join non-strings

x in lst                                 # O(n) — convert to set if repeated
lst.index(x)                             # first index, ValueError if absent
lst.count(x)

# init pitfalls
grid = [[0] * 4 for _ in range(3)]       # RIGHT
grid = [[0] * 4] * 3                     # WRONG: 3 refs to the SAME row
```

## Dicts

```python
d = {"a": 1, "b": 2}
d.get(k)                                 # None if missing (no raise)
d.get(k, default)
d.setdefault(k, []).append(x)            # init-if-missing, then use
d.pop(k, None)                           # remove, no raise

merged = d1 | d2                         # 3.9+ (d2 wins); in-place: d1 |= d2
{**d1, **d2, "extra": 1}                 # merge + add

{k: v * 2 for k, v in d.items()}         # dict comprehension
{v: k for k, v in d.items()}             # invert
{k: v for k, v in d.items() if v > 0}    # filter

for k, v in d.items(): ...               # ALWAYS items() for both
max(d, key=d.get)                        # key with max value
dict(sorted(d.items()))                  # sorted by key
dict(sorted(d.items(), key=lambda kv: kv[1]))   # sorted by value

from collections import defaultdict, Counter
groups = defaultdict(list)
for u in users:
    groups[u.dept].append(u)             # no key-exists check ever

c = Counter("abracadabra")               # {'a': 5, 'b': 2, ...}
c.most_common(2)                         # [('a', 5), ('b', 2)]
c["z"]                                   # 0, never KeyError
Counter(a) - Counter(b), c1 + c2         # multiset ops

next(iter(d)), next(iter(d.values()))    # first key / value without pop
```

## Sets

```python
s = {1, 2, 3};  empty = set()            # {} is an empty DICT
s.add(x);  s.discard(x)                  # discard: no raise if absent
s | t,  s & t,  s - t,  s ^ t            # union, intersection, diff, sym-diff
s <= t,  s < t                           # subset, proper subset
s.isdisjoint(t)

{x.id for x in items}                    # set comprehension
seen = set()                             # classic dedupe-while-looping
for x in stream:
    if x.key in seen: continue
    seen.add(x.key)
```

## Strings

```python
s.split()                    # on ANY whitespace, no empty strings
s.split(",", maxsplit=1)     # "k=v" style: k, _, v = s.partition("=")
s.splitlines()
s.strip(), s.strip(".,!")    # both ends; lstrip/rstrip
s.removeprefix("id_"), s.removesuffix(".txt")     # 3.9+
s.replace(old, new)
s.startswith(("http://", "https://"))    # tuple = OR
s.lower(), s.casefold()                  # casefold for comparisons
s.zfill(5), s.rjust(10), s.ljust(10, "-"), s.center(20)
"".join(reversed(s)), s[::-1]            # reverse
s.isdigit(), s.isalpha(), s.isalnum(), s.isidentifier()
"sep".join(parts)                        # NEVER += in a loop

# f-strings
f"{x!r}"                     # repr
f"{x=}"                      # debug: prints  x=42
f"{n:,}"  f"{n:_}"           # 1,234,567 / 1_234_567
f"{ratio:.1%}"               # 12.3%
f"{x:.2f}"  f"{x:8.2f}"      # 2 decimals / width 8
f"{n:05d}"  f"{n:#x}"  f"{n:b}"          # 00042 / 0x2a / binary
f"{s:>10}" f"{s:<10}" f"{s:^10}"         # align right/left/center
f"{dt:%Y-%m-%d %H:%M}"                   # datetime inline
f"{'a' if cond else 'b'}"                # expressions allowed
```

## Unpacking & assignment

```python
a, b = b, a                              # swap
a, b, *rest = [1, 2, 3, 4, 5]            # rest = [3, 4, 5]
first, *_, last = seq
(a, b), c = pair, x                      # nested

def f(*args, **kwargs): ...
f(*lst, **d)                             # unpack into call
[*a, *b],  (*a, *b),  {**d1, **d2}       # merge literals

for i, x in enumerate(items, start=1): ...
for a, b in zip(xs, ys): ...
for a, b in zip(xs, ys, strict=True): ...   # 3.10+: raise on length mismatch
list(zip(*matrix))                       # transpose
dict(zip(keys, values))

x, = single_item_list                    # or [x] = ...; raises if != 1 element
```

## Walrus operator `:=`

```python
if (n := len(data)) > 10:                # use a value you also test
    print(f"too long: {n}")

while (chunk := f.read(8192)):           # read-until-empty loops
    process(chunk)

if (m := re.search(r"\d+", s)):          # regex match-and-use
    print(m.group())

[y for x in data if (y := expensive(x)) is not None]   # compute once in comp
```

## Conditionals & loops

```python
x = a if cond else b                     # ternary
x = val or default                       # careful: 0/""/[] also fall through
x = val if val is not None else default  # the safe version

0 <= i < len(lst)                        # chained comparison
if x in ("a", "b", "c"): ...             # membership instead of or-chains

for x in reversed(lst): ...
for i in range(len(lst) - 1, -1, -1): ...   # index-based reverse

for x in items:
    if match(x):
        found = x
        break
else:                                    # for-else: runs iff NO break
    raise LookupError("not found")

# but usually cleaner:
found = next((x for x in items if match(x)), None)

any(x > 0 for x in nums)                 # short-circuits
all(u.active for u in users)             # True on empty!
sum(1 for x in items if pred(x))         # count matching
```

## collections & friends

```python
from collections import deque, namedtuple
dq = deque(maxlen=100)                   # rolling window / recent-N
dq.appendleft(x); dq.pop(); dq.rotate(1) # O(1) both ends

Point = namedtuple("Point", "x y")       # or dataclass for anything richer
p = Point(1, 2); p.x; x, y = p

from enum import Enum, auto
class Status(Enum):
    ACTIVE = auto()
    DONE = auto()
Status.ACTIVE.name                       # "ACTIVE"
Status["ACTIVE"]                         # lookup by name
Status(1)                                # lookup by value

import itertools as it
it.chain(a, b)                           # concat iterables lazily
it.islice(gen, 10)                       # first 10 of a generator (no slicing!)
it.batched(data, 3)                      # 3.12+: chunks of 3
it.pairwise(seq)                         # (s0,s1), (s1,s2), ...  3.10+
it.groupby(sorted(rows, key=k), key=k)   # MUST sort by same key first
it.product(range(3), repeat=2)           # nested loops flattened
it.combinations(items, 2)                # pairs, no repeats
it.count(10), it.cycle("AB"), it.repeat(x, 5)

from functools import reduce, cache
reduce(lambda acc, x: acc | x, sets)     # fold — but sum/any/max cover 90%
```

## Files & paths (pathlib)

```python
from pathlib import Path

p = Path("data") / "raw" / "file.txt"    # join with /
p.read_text()                            # whole file as str (encoding="utf-8")
p.write_text(s)                          # atomic enough for small stuff
p.exists(), p.is_file(), p.is_dir()
p.name, p.stem, p.suffix, p.parent       # file.txt / file / .txt / data/raw
p.with_suffix(".json")
p.mkdir(parents=True, exist_ok=True)
list(p.glob("*.csv")), list(p.rglob("*.py"))    # rglob = recursive
Path.home(), Path.cwd(), Path(__file__).parent

with open(p) as f:                       # line-by-line, constant memory
    for line in f:
        line = line.rstrip("\n")

import json
data = json.loads(p.read_text())
p.write_text(json.dumps(data, indent=2, default=str))   # default=str: dates etc.

import csv
with open(p, newline="") as f:
    for row in csv.DictReader(f): ...
```

## Numbers

```python
q, r = divmod(17, 5)                     # (3, 2)
7 // 2, -7 // 2                          # 3, -4  (floor, not truncation!)
7 % 3, -7 % 3                            # 1, 2   (sign follows divisor)
round(2.675, 2)                          # 2.67 — float repr, not a bug; use Decimal for money
int("ff", 16), int("1010", 2)            # from base
abs(x), pow(2, 10), 2**10
min(max(x, lo), hi)                      # clamp
math.inf, -math.inf                      # initial best-so-far values
float("nan") != float("nan")             # NaN never equals; use math.isnan
sum(xs) / len(xs), statistics.mean(xs), statistics.median(xs)
x != int(x)                              # has decimal part
f"{0.1 + 0.2:.17f}"                      # why == fails on floats
math.isclose(a, b)
random.choice(seq), random.sample(seq, 3), random.shuffle(lst), random.randint(1, 6)
```

## Datetime quickies

```python
from datetime import datetime, date, timedelta, timezone

now = datetime.now(timezone.utc)         # ALWAYS aware in servers
today = date.today()
dt = datetime.fromisoformat("2026-07-07T12:00:00+00:00")
dt.isoformat()
dt + timedelta(days=7, hours=3)
(d2 - d1).days, (t2 - t1).total_seconds()
dt.strftime("%Y-%m-%d"), datetime.strptime(s, "%d/%m/%Y")
dt.timestamp(), datetime.fromtimestamp(ts, tz=timezone.utc)

import time
start = time.perf_counter()              # timing code: perf_counter,
elapsed = time.perf_counter() - start    # NEVER time.time()
```

## Regex quickies

```python
import re

m = re.search(r"(\d{4})-(\d{2})", s)     # first match anywhere
if m: m.group(0), m.group(1), m.groups()
re.match(...)                            # anchored at START only; fullmatch = whole string
re.findall(r"\d+", s)                    # all matches as list of strings
re.finditer(r"\d+", s)                   # lazy, gives match objects
re.sub(r"\s+", " ", s)                   # normalize whitespace
re.sub(r"(\w+)@(\w+)", r"\2.\1", s)      # backrefs in replacement
re.split(r"[,;]\s*", s)
pattern = re.compile(r"^\w+$")           # compile if reused in a loop
(?P<year>\d{4})  →  m.group("year")      # named groups
r"..." always                            # raw strings for patterns
```

## Misc that saves time

```python
print(f"{x=}, {y=}")                     # fastest debug print
breakpoint()                             # drops into pdb (c=continue, n=next)
from pprint import pprint; pprint(obj)
import sys; print(x, file=sys.stderr)

isinstance(x, (int, float))              # multi-type check
callable(f), hasattr(o, "attr"), getattr(o, "attr", default)
vars(obj), dir(obj)                      # inspect anything

id_gen = iter(range(10**9))              # quick unique ids: next(id_gen)

# swap keys case-insensitively, chain gets, etc.
value = (d.get("a") or {}).get("b")      # nested get without KeyError
value = d1.get(k) or d2.get(k) or default

import os
os.environ.get("DEBUG", "0") == "1"
os.cpu_count()

import subprocess
out = subprocess.run(["ls", "-l"], capture_output=True, text=True, check=True).stdout

import argparse                          # sys.argv[1] fine for one arg;
                                         # argparse the moment you have flags

# quick throwaway HTTP (stdlib only)
from urllib.request import urlopen
body = urlopen("https://api.example.com").read().decode()
```

## Complexity crib (know cold)

| Operation | Cost |
|---|---|
| `list` append / pop-end / index | O(1) |
| `list` insert(0) / pop(0) / `x in list` / remove | O(n) |
| `dict` / `set` get, put, `in` | O(1) avg |
| `sorted()` / `.sort()` | O(n log n) |
| slicing `lst[a:b]` | O(b-a) copy |
| `deque` append/pop both ends | O(1) |
| `heapq` push/pop | O(log n) |
| string concat in loop | O(n²) total — use join |
