Pythons innebygde filhåndtering er en av språkets genuine styrker — ingen imports nødvendig
for grunnleggende lese/skrive-operasjoner, og API-et er rent nok til å lære på en ettermiddag. Men det er et reelt
gap mellom opplæringsversjonen og det du faktisk ville sende til produksjon. Opplæringsversjonen åpner en fil, leser den
og lukker den. Produksjonsversjonen håndterer kodingsinkonsekvenser som stille korrumperer data, stier som
fungerer på macOS men eksploderer på Windows, og loggfiler som stille spiser opp all minnet ditt hvis du kaller
read()
på en 2 GB fil. Denne artikkelen dekker mønstrene som holder — ikke bare den glade veien.
with-setningen — bruk den alltid
Hvert filhåndteringseksempel i Python bør bruke en
kontekstbehandler
— with-blokken som sikrer at filen lukkes selv om et unntak oppstår midt i lesingen.
En kontekstbehandler er et objekt som definerer hva som skjer ved inngang og utgang fra en with-blokk;
for filer betyr utgang at close() kalles automatisk. Her er hvorfor det betyr noe i praksis:
# ❌ 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 blockPå langkjørende servere er dette ikke akademisk — lekkasje av filhåndtak fører til slutt til
OSError: [Errno 24] Too many open files. with-setningen koster ingenting
og forhindrer den feilklassen fullstendig. Bruk den overalt.
Lesing av filer — fire måter, ett riktig verktøy hver gang
Python gir deg flere metoder på et filobjekt, og å velge riktig betyr mer enn de fleste opplæringer innrømmer:
f.read()— leser hele filen inn i en enkelt streng. Greit for små konfigurasjonsfiler, farlig for store.f.readline()— leser én linje om gangen og fremfører den interne pekeren. Nyttig når du trenger manuell kontroll over iterasjon.f.readlines()— leser alle linjer inn i en liste. Praktisk, men laster fortsatt hele filen inn i minnet.for line in f:— iteratorprotokollen. Leser én linje om gangen uten å laste hele filen. Dette er den du bør bruke som standard.
Her er et realistisk eksempel: lesing av en .env-lignende konfigurasjonsfil og gjøre den om
til en ordbok. Dette er den slags ting du faktisk skriver, ikke en kunstig "les 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()-vanen: Når du leser linjer, inkluderer hver linje unntatt
den siste et etterfølgende \n (og på Windows, \r\n). Kall
line.strip() for å fjerne begge. Hvis du bare vil fjerne linjeskiftet og ikke ledende
mellomrom, bruk line.rstrip('\n') i stedet.Skriving og tillegg — kjenn hvilken modus som ødelegger data
Det andre argumentet til
open()
er modusen. To moduser snubler folk over gang på gang:
'w'— skrivemodus. Åpner filen for skriving. Hvis filen allerede eksisterer, avkortes den til null byte umiddelbart — før du skriver et eneste tegn. Dette er stille dataødeleggelse hvis du åpner feil sti.'a'— tillegsmodus. Åpner filen og flytter skrivepekeren til slutten. Eksisterende innhold berøres aldri. Nye skrivinger går etter det som allerede var der.
Et godt brukstilfelle for tilleggsmodus er å skrive en strukturert loggfil med tidsstempler. Her er et mønster som er nyttig i skript og små tjenester:
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') oppretter filen hvis den ikke eksisterer —
noe som er praktisk — men den ødelegger stille også filen hvis den eksisterer. En feilskrevet
sti kan slette en produksjonsfil uten noen feilmelding. Hvis du ikke er sikker på at filen skal
overskrives, sjekk først med Path(path).exists() eller bruk 'x'-modus, som
hever FileExistsError i stedet for å overskrive.Koding — feilen som biter alle til slutt
Dette er den aller vanligste kilden til stille datakorrupsjon i Python-filhåndtering.
Python 3s standardkoding når du kaller open() uten å spesifisere én, bestemmes
av locale.getpreferredencoding() — som på Windows typisk er cp1252,
og på Linux/macOS vanligvis er UTF-8. Det betyr kode som fungerer perfekt på Mac-en din,
kan stille forvrenge eller krasje på en Windows-server når filen inneholder tegn utenfor ASCII.
Løsningen er ett ekstra 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()BOM-problemet er særlig vanlig med CSV-filer eksportert fra Microsoft Excel — filen
starter med et skjult \ufeff-tegn som vises som  hvis det
leses med feil koding, eller får den første kolonneoverskriften til å se ut som
name i stedet for name. Bruk av encoding='utf-8-sig'
håndterer det transparent. Se
Python-kodecs-dokumentasjonen
for den fullstendige listen over kodingsnavn.
encoding='utf-8' (eller
'utf-8-sig' for Excel-eksporter) til hvert open()-kall. Gjør det til en
vane — det koster ingenting og eliminerer en hel kategori av miljøspesifikke feil.Arbeid med stier — bruk pathlib
Den gamle måten å bygge filstier i Python var strengsammenkobling eller
os.path.join(). Den moderne måten er
pathlib.Path,
tilgjengelig siden Python 3.4 og fullt moden siden 3.6. Den håndterer stiskilletegn korrekt
på Windows og Unix uten at du tenker på det, og den erstatter en håndfull
os.path-kall med lesbar attributttilgang.
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"/-operatoren er ikke divisjon her — Path overstyrer den til
å bety stisammenkobling. Det leses naturlig og eliminerer anførsels- og skilletegnsproblemene som kommer
med strengbasert stibygging. En mer nyttig metode: path.read_text(encoding='utf-8')
er en snarvei for åpne/lese/lukke-mønsteret når du bare vil ha filens innhold som en streng.
Lesing av store filer uten å sprenge minnet
Når en fil er liten — si under noen megabyte — er f.read() eller
f.readlines() greit. Når det er en 500 MB serverlogg eller en multi-gigabyte dataeksport,
er det å laste hele greia inn i minnet en rask vei til en MemoryError eller
et prosesskill fra OS. Løsningen er linje-for-linje-iterasjon:
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)}")for line in f:-mønsteret fungerer fordi Pythons filobjekt implementerer
iteratorprotokollen — det henter linjer fra disk én om gangen ved hjelp av en intern buffer, så
minneforbruket er i det vesentlige konstant uavhengig av filstørrelse. For virkelig massive filer (titalls av
gigabyte) der selv linje-for-linje-iterasjon ikke er rask nok,
mmap
lar deg minnemappe filen og søke i den med regulære uttrykk uten å lese den i det hele tatt —
men for de fleste brukstilfeller er linjeiteratoren alt du trenger.
Lesing og skriving av JSON og CSV
To formater dukker opp konstant i ekte Python-arbeid, og begge har dedikerte stdlib-moduler som håndterer anføring, escaping og struktur riktig — ikke parse dem med strengdelinger.
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)Noen ting verdt å merke seg: send newline='' når du åpner CSV-filer —
csv-modulen
håndterer sine egne linjeavslutninger, og å la Pythons universelle nylinjemodus forstyrre forårsaker
doble tomme rader på Windows. For
JSON,
lar ensure_ascii=False ikke-ASCII-tegn (aksentuerte bokstaver, CJK-tegn osv.) skrives
som de er i stedet for å bli escapet til \uXXXX-sekvenser — mye mer
lesbar utdata. Hvis du jobber med JSON- eller CSV-data og vil inspisere eller transformere det
visuelt, er JSON Formatter og
CSV Formatter på dette nettstedet gode komplementer til kode-tilnærmingen.
Feilhåndtering — de tre unntakene du vil se
Filoperasjoner mislykkes på forutsigbare måter. Å håndtere hvert tilfelle eksplisitt gir deg feilmeldinger som faktisk er nyttige i stedet for en generisk 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 fire unntakene dekker nesten alle reelle feilmodi: filen eksisterer ikke,
du har ikke tillatelse, kodingen er feil, eller innholdet er misformet. Hver melding
forteller den neste utvikleren (eller deg klokken 2 om morgenen) nøyaktig hva som gikk galt og hvor du skal se.
Å fange et bart Exception og skrive ut "noe gikk galt" er ikke nyttig feilhåndtering —
det bare flytter forvirringen nedstrøms.
FileNotFoundError
og returnere None eller en standardordbok. Velg én atferd per funksjon
og dokumenter den.Oppsummering
Den korte versjonen av alt ovenfor: bruk alltid with-blokker, send alltid
encoding='utf-8', bruk pathlib.Path for stibygging, og
iterer linjer i stedet for å lese hele filer når størrelsen er ukjent. Disse fire vanene eliminerer
det store flertallet av filhåndteringsfeil før de når produksjon.
For dypere lesing: Python-opplærings-seksjonen om lesing og skriving av filer dekker det grunnleggende grundig. pathlib-dokumentasjonen er verdt å bokmerke — det er en av de mest nyttige delene av stdlib, og de fleste Python-utviklere underutnytter det. csv-moduldokumentasjonen og json-moduldokumentasjonen har begge gode eksempler for kanttilfeller (tilpassede skilletegn, strømmende JSON osv.) verdt å lese hvis du jobber med disse formatene regelmessig.