Python Cheatsheet
JSON and CSV
Use this Python reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
json Core Four
dumps/loads work on strings, dump/load work on file objects. The s is for
string.
import json json.dumps(obj) # object -> str json.loads(text) # str, bytes, or bytearray -> object json.dump(obj, fp) # object -> file json.load(fp) # file -> object with open("data.json", "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) with open("data.json", encoding="utf-8") as f: data = json.load(f) from pathlib import Path data = json.loads(Path("data.json").read_text(encoding="utf-8")) Path("out.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
Serialization Parameters
| Parameter | Default | Effect |
|---|---|---|
indent=2 | None | pretty-print, None gives the compact one-liner |
sort_keys=True | False | deterministic key order, good for diffs and hashes |
separators=(",", ":") | (", ", ": ") | drop all whitespace, smallest payload |
ensure_ascii=False | True | emit real UTF-8 instead of ü escapes |
default=fn | None | called for any object json cannot handle |
cls=MyEncoder | JSONEncoder | full encoder subclass |
allow_nan=False | True | raise instead of emitting NaN/Infinity |
skipkeys=True | False | drop non-string keys instead of raising |
check_circular=False | True | skip the cycle check, faster and riskier |
json.dumps({"a": 1, "b": [1, 2]}, separators=(",", ":")) # '{"a":1,"b":[1,2]}' json.dumps({"s": "ü"}, ensure_ascii=False) # '{"s": "ü"}' json.dumps(float("nan")) # 'NaN', INVALID JSON json.dumps(float("nan"), allow_nan=False) # ValueError: Out of range float values are not JSON compliant: nan
NaN, Infinity, and -Infinity are Python extensions that no other language
accepts. Pass allow_nan=False on anything leaving your process.
Deserialization hooks:
| Parameter | Effect |
|---|---|
object_hook=fn | called with every decoded dict, return the replacement |
object_pairs_hook=fn | called with the key/value pairs, wins over object_hook |
parse_float=Decimal | exact decimals instead of binary floats, for money |
parse_int=fn | e.g. a bounds check on huge integers |
strict=False | allow raw control characters inside strings |
json.loads('{"x": 1.5}', parse_float=Decimal) # {'x': Decimal('1.5')} json.loads('{"a": 1}', object_pairs_hook=list) # [('a', 1)] json.loads("1e1000") # inf, silently
Type Mapping
| Python | JSON | Round trip |
|---|---|---|
dict | object | yes |
list | array | yes |
tuple | array | no, comes back as a list |
str | string | yes |
int | number | yes (arbitrary precision on the way out) |
float | number | yes, with float precision |
True / False | true / false | yes |
None | null | yes |
set, bytes, datetime, Decimal, complex | — | TypeError |
Dict keys are coerced to strings, so {1: "a"} becomes {"1": "a"} and comes back
with a string key. A tuple key raises
TypeError: keys must be str, int, float, bool or None, not tuple.
Encoding What json Refuses
One default function covers everything. It is only called for objects the encoder
cannot handle, so keep it cheap.
import json, dataclasses from datetime import date, datetime from decimal import Decimal from uuid import UUID from pathlib import Path def json_default(o): if isinstance(o, (datetime, date)): return o.isoformat() if isinstance(o, Decimal): return str(o) # str, not float, to keep precision if isinstance(o, (set, frozenset)): return sorted(o) if isinstance(o, (UUID, Path)): return str(o) if isinstance(o, bytes): return o.decode("utf-8", "replace") if dataclasses.is_dataclass(o) and not isinstance(o, type): return dataclasses.asdict(o) raise TypeError(f"{type(o).__name__} is not JSON serializable") json.dumps({"u": User("a", date(2026, 1, 1)), "d": Decimal("1.5"), "s": {2, 1}}, default=json_default) # '{"u": {"name": "a", "joined": "2026-01-01"}, "d": "1.5", "s": [1, 2]}' # Same logic as a reusable class class Encoder(json.JSONEncoder): def default(self, o): return json_default(o) json.dumps(payload, cls=Encoder, indent=2)
Decoding back into typed objects:
from dataclasses import dataclass, fields @dataclass class User: name: str joined: date def to_user(d: dict) -> User: return User(name=d["name"], joined=date.fromisoformat(d["joined"])) user = to_user(json.loads(text)) # Ignore unknown keys instead of blowing up on **d known = {f.name for f in fields(User)} User(**{k: v for k, v in raw.items() if k in known})
For anything nested or externally supplied, use pydantic
(Model.model_validate_json(text)). It validates, coerces, and reports the exact
field path on failure, which hand-rolled decoding does not.
Errors
try: data = json.loads(text) except json.JSONDecodeError as e: # subclass of ValueError print(e.msg, e.pos, e.lineno, e.colno) print(e.doc[max(0, e.pos - 40):e.pos + 40]) # the offending window
json.JSONDecodeError covers bad syntax. A valid document with the wrong shape
raises KeyError or TypeError later, so validate after parsing.
json.dumps raises TypeError for unsupported types and ValueError for a
circular reference.
JSON Lines and Large Files
One JSON object per line, no wrapping array. It streams, it appends, and a single
corrupt line does not destroy the file. The de facto format for logs and datasets
(.jsonl, .ndjson).
# Write, appending safely with open("events.jsonl", "a", encoding="utf-8") as f: for rec in records: f.write(json.dumps(rec, ensure_ascii=False) + "\n") # Read lazily, one record at a time, constant memory def read_jsonl(path): with open(path, encoding="utf-8") as f: for lineno, line in enumerate(f, 1): line = line.strip() if not line: continue try: yield json.loads(line) except json.JSONDecodeError as e: print(f"skipping line {lineno}: {e.msg}") for rec in read_jsonl("events.jsonl"): process(rec)
json.load has no streaming mode, it builds the entire object in memory, and peak
usage runs several times the file size. For a giant single-document file use
ijson (ijson.items(f, "records.item")) or convert it to JSON Lines once.
Faster drop-ins for hot paths:
| Library | Notes |
|---|---|
orjson | fastest, dumps returns bytes, serializes datetime/UUID/dataclass natively, no indent=n (only OPT_INDENT_2) |
ujson | fast, mostly compatible, historically loose on edge cases |
msgspec | fast plus schema validation, typed decoding |
stdlib json | C-accelerated already, and always available |
import orjson orjson.dumps(obj, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS) # bytes orjson.loads(raw_bytes)
csv Reading
Always pass newline="" when opening a CSV file. The csv module handles line
endings itself, and without it a quoted field containing \r\n is split into two
rows on read, and every row gets a blank row after it on Windows write.
import csv with open("data.csv", newline="", encoding="utf-8") as f: reader = csv.reader(f) header = next(reader) # consume the header row for row in reader: # row is a list[str], ALWAYS strings name, age = row[0], int(row[1]) with open("data.csv", newline="", encoding="utf-8") as f: for row in csv.DictReader(f): # row is a dict keyed by the header print(row["name"], int(row["age"])) # No header in the file? Supply one. csv.DictReader(f, fieldnames=["name", "age"]) reader.line_num # physical lines consumed so far reader.fieldnames # DictReader only
Everything the reader yields is a string. There is no type inference, "" and a
truly empty field are indistinguishable, and csv does not skip comment lines.
csv Writing
with open("out.csv", "w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow(["name", "age"]) w.writerows([["Ada", 36], ["Alan", 41]]) with open("out.csv", "w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=["name", "age"], extrasaction="ignore", # drop unknown keys, default raises restval="") # fill missing keys w.writeheader() # easy to forget w.writerow({"name": "Ada", "age": 36}) w.writerows(rows)
Quoting and escaping happen for you. A field with a comma or a quote comes out as
"b,c" and "say ""hi""", and None is written as an empty field.
| Option | Default | Meaning |
|---|---|---|
delimiter | "," | "\t" for TSV, ";" for European exports |
quotechar | '"' | — |
doublequote | True | " inside a field becomes "" |
escapechar | None | required with QUOTE_NONE |
quoting | QUOTE_MINIMAL | see below |
lineterminator | "\r\n" | what the writer emits, the reader accepts both |
skipinitialspace | False | drop the space after a delimiter |
strict | False | raise csv.Error on a malformed field |
quoting constants: QUOTE_MINIMAL (only when needed), QUOTE_ALL,
QUOTE_NONNUMERIC (quote strings, and read unquoted fields as floats),
QUOTE_NONE (needs escapechar), plus QUOTE_STRINGS and QUOTE_NOTNULL (3.12).
csv.writer(f, delimiter="\t", quoting=csv.QUOTE_MINIMAL) # TSV csv.reader(f, dialect="excel-tab") # same, by dialect csv.list_dialects() # ['excel', 'excel-tab', 'unix'] csv.register_dialect("pipes", delimiter="|", quoting=csv.QUOTE_ALL)
Dialects, Limits, and Encoding
# Sniffer: guess the delimiter and whether there is a header with open("unknown.csv", newline="", encoding="utf-8") as f: sample = f.read(4096) f.seek(0) dialect = csv.Sniffer().sniff(sample) # dialect.delimiter -> ';' has_header = csv.Sniffer().has_header(sample) reader = csv.reader(f, dialect) # One huge field, such as an embedded blob csv.field_size_limit() # 131072 by default csv.field_size_limit(10 * 1024 * 1024) # _csv.Error: field larger than field limit (131072)
Encoding is where real CSVs die:
- Excel on Windows writes a UTF-8 BOM. Open with encoding="utf-8-sig", which
strips it, otherwise your first column name is "name".
- To produce a file Excel opens cleanly, write with encoding="utf-8-sig".
- Legacy exports are often cp1252 or latin-1. latin-1 never raises, so it is
the last-resort fallback, and errors="replace" keeps a broken file readable.
with open("excel.csv", newline="", encoding="utf-8-sig") as f: rows = list(csv.DictReader(f))
When To Use pandas Instead
csv is the right tool for streaming, for exact control over quoting, and for
files larger than memory. Switch to pandas when you want types, joins, grouping, or
analysis.
import pandas as pd df = pd.read_csv("data.csv", dtype={"zip": str}, parse_dates=["created_at"]) df.to_csv("out.csv", index=False) # index=False, always pd.read_csv("data.csv", chunksize=100_000) # iterator of DataFrames pd.read_json("data.jsonl", lines=True)
dtype={"zip": str} is not optional. Without it pandas turns "01234" into the
integer 1234, and long ID columns into floats.