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

ParameterDefaultEffect
indent=2Nonepretty-print, None gives the compact one-liner
sort_keys=TrueFalsedeterministic key order, good for diffs and hashes
separators=(",", ":")(", ", ": ")drop all whitespace, smallest payload
ensure_ascii=FalseTrueemit real UTF-8 instead of ü escapes
default=fnNonecalled for any object json cannot handle
cls=MyEncoderJSONEncoderfull encoder subclass
allow_nan=FalseTrueraise instead of emitting NaN/Infinity
skipkeys=TrueFalsedrop non-string keys instead of raising
check_circular=FalseTrueskip 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:

ParameterEffect
object_hook=fncalled with every decoded dict, return the replacement
object_pairs_hook=fncalled with the key/value pairs, wins over object_hook
parse_float=Decimalexact decimals instead of binary floats, for money
parse_int=fne.g. a bounds check on huge integers
strict=Falseallow 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

PythonJSONRound trip
dictobjectyes
listarrayyes
tuplearrayno, comes back as a list
strstringyes
intnumberyes (arbitrary precision on the way out)
floatnumberyes, with float precision
True / Falsetrue / falseyes
Nonenullyes
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:

LibraryNotes
orjsonfastest, dumps returns bytes, serializes datetime/UUID/dataclass natively, no indent=n (only OPT_INDENT_2)
ujsonfast, mostly compatible, historically loose on edge cases
msgspecfast plus schema validation, typed decoding
stdlib jsonC-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.

OptionDefaultMeaning
delimiter",""\t" for TSV, ";" for European exports
quotechar'"'—
doublequoteTrue" inside a field becomes ""
escapecharNonerequired with QUOTE_NONE
quotingQUOTE_MINIMALsee below
lineterminator"\r\n"what the writer emits, the reader accepts both
skipinitialspaceFalsedrop the space after a delimiter
strictFalseraise 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.