De ingebouwde bestandsverwerking van Python is een van de echte sterke punten van de taal — geen imports nodig
voor basale lees-/schrijfbewerkingen, en de API is schoon genoeg om in een middag te leren. Maar er is een echt
verschil tussen de tutorialversie en wat je daadwerkelijk zou verzenden. De tutorialversie opent een bestand, leest het
en sluit het. De productieversie omgaat met codeeringsfouten die gegevens stilzwijgend beschadigen, paden die
werken op macOS maar crashen op Windows, en logbestanden die stilletjes al je geheugen opslokken als je
read()
aanroept op een bestand van 2 GB. Dit artikel behandelt de patronen die standhouden — niet alleen het mooie pad.
De with-instructie — Altijd Gebruiken
Elk voorbeeld van bestandsverwerking in Python zou een
contextmanager
moeten gebruiken — het with-blok dat ervoor zorgt dat het bestand wordt gesloten, zelfs als er een uitzondering wordt gegenereerd tijdens het lezen.
Een contextmanager is een object dat bepaalt wat er gebeurt bij het betreden en verlaten van een with-blok;
voor bestanden betekent verlaten dat close() automatisch wordt aangeroepen. Dit is waarom het in de praktijk belangrijk is:
# ❌ Manual close — works until it doesn't
f = open('app.log')
data = f.read() # if this raises an exception...
f.close() # ...this line never runs. File handle leaks.
# ✅ Context manager — close() is guaranteed
with open('app.log') as f:
data = f.read()
# file is closed here, no matter what happened inside the blockOp langlopende servers is dit niet academisch — het lekken van bestandshandvaten veroorzaakt uiteindelijk
OSError: [Errno 24] Too many open files. De with-instructie kost niets
en voorkomt deze klasse van bugs volledig. Gebruik het overal.
Bestanden Lezen — Vier Manieren, Één Juist Hulpmiddel Per Keer
Python biedt je verschillende methoden op een bestandsobject, en de juiste kiezen doet er meer toe dan de meeste tutorials toegeven:
f.read()— leest het hele bestand in één enkele string. Prima voor kleine configuratiebestanden, gevaarlijk voor grote.f.readline()— leest één regel tegelijk, waardoor de interne aanwijzer vooruit beweegt. Nuttig wanneer je handmatige controle over iteratie nodig hebt.f.readlines()— leest alle regels in een lijst. Handig, maar laadt nog steeds het hele bestand in het geheugen.for line in f:— het iteratorprotocol. Leest één regel tegelijk zonder het volledige bestand te laden. Dit is de standaard aanpak.
Hier is een realistisch voorbeeld: een configuratiebestand in .env-stijl lezen en omzetten
naar een woordenboek. Dit is het soort ding dat je daadwerkelijk schrijft, niet een kunstmatige "lees hello.txt"-demo:
from pathlib import Path
def load_config(path: str) -> dict:
"""Read a key=value config file, ignoring comments and blank lines."""
config = {}
with open(path, encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' not in line:
continue
key, _, value = line.partition('=')
config[key.strip()] = value.strip()
return config
# Usage
settings = load_config('config/app.conf')
db_host = settings.get('DB_HOST', 'localhost').strip()-gewoonte: Bij het lezen van regels bevat elke regel behalve de
laatste een afsluitende \n (en op Windows, \r\n). Roep
line.strip() aan om beide te verwijderen. Als je alleen de newline wilt verwijderen en niet de voorloopspaties,
gebruik dan line.rstrip('\n').Schrijven en Toevoegen — Weet Welke Modus Gegevens Vernietigt
Het tweede argument van
open()
is de modus. Twee modi struikelen mensen herhaaldelijk:
'w'— schrijfmodus. Opent het bestand voor schrijven. Als het bestand al bestaat, wordt het onmiddellijk afgekapt tot nul bytes — voordat je één teken schrijft. Dit is stille gegevensvernietiging als je het verkeerde pad opent.'a'— toevoegmodus. Opent het bestand en verplaatst de schrijfaanwijzer naar het einde. Bestaande inhoud wordt nooit aangeraakt. Nieuwe schrijfbewerkingen gaan na wat er al was.
Een goed gebruik voor de toevoegmodus is het schrijven van een gestructureerd logbestand met tijdstempels. Hier is een patroon dat nuttig is in scripts en kleine services:
import datetime
LOG_FILE = 'logs/pipeline.log'
def log_event(level: str, message: str) -> None:
timestamp = datetime.datetime.utcnow().isoformat() + 'Z'
line = f"[{timestamp}] {level.upper()}: {message}\n"
with open(LOG_FILE, 'a', encoding='utf-8') as f:
f.write(line)
log_event('info', 'Pipeline started')
log_event('warning', 'Retrying connection to database')
log_event('error', 'Failed to parse row 4821 — skipping')open(path, 'w') maakt het bestand aan als het niet bestaat —
wat handig is — maar het vernietigt stilzwijgend het bestand als het wel bestaat. Een verkeerd getypt
pad kan een productiebestand wissen zonder enig foutbericht. Als je niet zeker weet of het bestand overschreven mag worden,
controleer dan eerst met Path(path).exists() of gebruik de modus 'x', die
FileExistsError gooit in plaats van te overschrijven.Codering — De Bug Die Iedereen Uiteindelijk Bijt
Dit is de meest voorkomende bron van stille gegevensbeschadiging bij Python-bestandsverwerking.
De standaardcodering van Python 3 wanneer je open() aanroept zonder er een op te geven, wordt bepaald
door locale.getpreferredencoding() — wat op Windows meestal cp1252 is,
en op Linux/macOS gewoonlijk UTF-8. Dat betekent dat code die perfect werkt op je Mac
stilzwijgend kan beschadigen of crashen op een Windows-server wanneer het bestand tekens buiten ASCII bevat.
De oplossing is één extra argument:
# ❌ Platform-dependent — works on Linux, corrupts on Windows
with open('customers.csv') as f:
data = f.read()
# ✅ Explicit UTF-8 — same behavior on every platform
with open('customers.csv', encoding='utf-8') as f:
data = f.read()
# For files exported from Excel on Windows — may have a BOM (byte order mark)
# utf-8-sig strips the BOM automatically on read
with open('export.csv', encoding='utf-8-sig') as f:
data = f.read()Het BOM-probleem is bijzonder gebruikelijk bij CSV-bestanden die zijn geëxporteerd vanuit Microsoft Excel — het
bestand begint met een verborgen \ufeff-teken dat als  verschijnt als
gelezen met de verkeerde codering, of zorgt ervoor dat de eerste kolomkop eruitziet als
name in plaats van name. Het gebruik van encoding='utf-8-sig'
behandelt dit transparant. Zie de
Python codecs-documentatie
voor de volledige lijst van coderingsnamen.
encoding='utf-8' door (of
'utf-8-sig' voor Excel-exports) aan elke open()-aanroep. Maak er een gewoonte van
— het kost niets en elimineert een hele categorie omgevingsspecifieke bugs.Werken met Paden — Gebruik pathlib
De oude manier om bestandspaden in Python te bouwen was stringconcatenatie of
os.path.join(). De moderne manier is
pathlib.Path,
beschikbaar vanaf Python 3.4 en volledig volwassen vanaf 3.6. Het verwerkt padscheidingstekens correct
op Windows en Unix zonder dat je erover hoeft na te denken, en het vervangt een handvol
os.path-aanroepen door leesbare attribuuttoegang.
from pathlib import Path
# Build a path relative to the current script — works on Windows and Unix
base_dir = Path(__file__).parent
data_dir = base_dir / 'data'
input_file = data_dir / 'records.csv'
# Check existence before opening
if not input_file.exists():
raise FileNotFoundError(f"Input file not found: {input_file}")
# Create a directory (including parents) without error if it already exists
output_dir = base_dir / 'output' / 'reports'
output_dir.mkdir(parents=True, exist_ok=True)
# Iterate over all JSON files in a directory
for json_file in data_dir.glob('*.json'):
print(json_file.name) # just the filename: 'records.json'
print(json_file.stem) # filename without extension: 'records'
print(json_file.suffix) # extension: '.json'
print(json_file.parent) # parent directory as a Path
# The / operator builds paths — no os.path.join needed
report_path = output_dir / f"report_{input_file.stem}.txt"De /-operator is hier geen deling — Path overschrijft het om
padverbinding te betekenen. Dit leest natuurlijk en elimineert de aanhalingstekens- en scheidingstekenproblemen die gepaard gaan
met op strings gebaseerde padbouw. Nog een nuttige methode: path.read_text(encoding='utf-8')
is een snelkoppeling voor het open/lees/sluit-patroon wanneer je alleen de inhoud van het bestand als string wilt.
Grote Bestanden Lezen Zonder Het Geheugen Te Overbelasten
Wanneer een bestand klein is — laten we zeggen, onder een paar megabyte — is f.read() of
f.readlines() prima. Wanneer het een serverlogboek van 500 MB of een multi-gigabyte gegevensexport is,
is het hele ding in het geheugen laden een snelle weg naar een MemoryError of
een procesbeëindiging door het OS. De oplossing is regel-voor-regel iteratie:
from pathlib import Path
from collections import Counter
def count_error_levels(log_path: str) -> dict:
"""
Process a large log file line by line.
Memory usage stays roughly constant regardless of file size.
"""
counts = Counter()
with open(log_path, encoding='utf-8') as f:
for line in f:
# Each line is fetched from disk as needed — not loaded all at once
if ' ERROR ' in line:
counts['error'] += 1
elif ' WARN ' in line:
counts['warning'] += 1
elif ' INFO ' in line:
counts['info'] += 1
return dict(counts)
results = count_error_levels('/var/log/app/server.log')
print(f"Errors: {results.get('error', 0)}, Warnings: {results.get('warning', 0)}")Het patroon for line in f: werkt omdat het bestandsobject van Python het iteratorprotocol
implementeert — het haalt regels één voor één van de schijf op met behulp van een interne buffer, zodat
het geheugengebruik in wezen constant is ongeacht de bestandsgrootte. Voor echt grote bestanden (tientallen
gigabytes) waarbij zelfs regel-voor-regel iteratie niet snel genoeg is,
mmap
stelt je in staat het bestand in het geheugen te mappen en het te doorzoeken met reguliere expressies zonder het te lezen —
maar voor de meeste gebruiksscenario's is de regeliterator alles wat je nodig hebt.
JSON en CSV Lezen en Schrijven
Twee formaten komen constant voor in echt Python-werk, en beide hebben speciale stdlib-modules die aanhalingstekens, escaping en structuur correct verwerken — parseer ze niet met stringsplitsingen.
import json
import csv
from pathlib import Path
# --- JSON ---
# Reading
with open('config/settings.json', encoding='utf-8') as f:
settings = json.load(f) # parsed directly from the file object
# Writing (indent=2 gives readable output)
with open('output/results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
# --- CSV ---
# Reading
with open('data/customers.csv', encoding='utf-8-sig', newline='') as f:
reader = csv.DictReader(f) # each row is a dict keyed by header
for row in reader:
process_customer(row['email'], row['plan'])
# Writing
fieldnames = ['id', 'email', 'plan', 'created_at']
with open('output/export.csv', 'w', encoding='utf-8', newline='') as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for record in records:
writer.writerow(record)Een paar dingen om op te merken: geef newline='' door bij het openen van CSV-bestanden —
de csv-module
verwerkt zijn eigen regeleinden, en door Python's universele newline-modus te laten interfereren ontstaan
dubbele lege rijen op Windows. Voor
JSON,
ensure_ascii=False laat niet-ASCII-tekens (geaccentueerde letters, CJK-tekens,
enz.) als zodanig schrijven in plaats van te worden geëscaped naar \uXXXX-reeksen — veel beter
leesbare uitvoer. Als je werkt met JSON- of CSV-gegevens en deze visueel wilt inspecteren of transformeren,
zijn de JSON Formatter en
CSV Formatter op deze site goede aanvullingen op de
codebenadering.
Foutafhandeling — De Drie Uitzonderingen Die Je Zult Zien
Bestandsbewerkingen mislukken op voorspelbare manieren. Elk geval expliciet afhandelen geeft je foutmeldingen die daadwerkelijk nuttig zijn in plaats van een generieke traceback:
import json
from pathlib import Path
def load_json_config(path: str) -> dict:
"""
Load a JSON config file with explicit error handling.
Returns the parsed config or raises with a clear message.
"""
config_path = Path(path)
try:
with open(config_path, encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
raise FileNotFoundError(
f"Config file not found: {config_path.resolve()}\n"
f"Create it or set CONFIG_PATH to the correct location."
)
except PermissionError:
raise PermissionError(
f"No read permission on {config_path.resolve()}\n"
f"Check file ownership and mode (chmod 644 on Linux)."
)
except UnicodeDecodeError as e:
raise ValueError(
f"Encoding error reading {config_path}: {e}\n"
f"Try opening with encoding='utf-8-sig' if the file came from Windows."
)
except json.JSONDecodeError as e:
raise ValueError(
f"Invalid JSON in {config_path} at line {e.lineno}, col {e.colno}: {e.msg}"
)De vier uitzonderingen dekken bijna elke echte faalwijze: het bestand bestaat niet,
je hebt geen toestemming, de codering is verkeerd, of de inhoud is misvormd. Elk bericht
vertelt de volgende ontwikkelaar (of jij om 2 uur 's nachts) precies wat er misging en waar te kijken. Een kale
Exception vangen en "iets ging fout" afdrukken is geen nuttige foutafhandeling —
het verplaatst alleen de verwarring stroomafwaarts.
FileNotFoundError
en het retourneren van None of een standaard dict prima. Kies één gedrag per functie
en documenteer het.Afsluiting
De korte versie van alles hierboven: gebruik altijd with-blokken, geef altijd
encoding='utf-8' door, gebruik pathlib.Path voor padopbouw, en
itereer over regels in plaats van hele bestanden te lezen wanneer de grootte onbekend is. Deze vier gewoonten elimineren
de overgrote meerderheid van bestandsverwerkingsbugs voordat ze de productie bereiken.
Voor diepgaander lezen: de Python-tutorialsectie over het lezen en schrijven van bestanden behandelt de basisbeginselen grondig. De pathlib-documentatie is het bookmarken waard — het is een van de nuttigste delen van de stdlib en de meeste Python-ontwikkelaars gebruiken het te weinig. De csv-moduledocumentatie en json-moduledocumentatie hebben beide goede voorbeelden voor randgevallen (aangepaste scheidingstekens, streaming JSON, enz.) die de moeite waard zijn te lezen als je regelmatig met die formaten werkt.