Wbudowana obsługa plików Pythona to jedna z prawdziwych mocnych stron języka — nie są potrzebne żadne importy do podstawowych operacji odczytu/zapisu, a API jest na tyle czyste, że można je nauczyć w ciągu popołudnia. Ale istnieje prawdziwa luka między wersją z samouczka a tym, co faktycznie wdrożysz. Wersja z samouczka otwiera plik, czyta go i zamyka. Wersja produkcyjna radzi sobie z niezgodnościami kodowania, które po cichu uszkadzają dane, ścieżkami, które działają na macOS, ale wysypują się na Windows, i plikami logów, które cicho zjadają całą pamięć, jeśli wywołasz read() na pliku o rozmiarze 2 GB. Ten artykuł omawia wzorce, które wytrzymują — nie tylko szczęśliwą ścieżkę.
Instrukcja with — używaj jej zawsze
Każdy przykład obsługi plików w Pythonie powinien używać menedżera kontekstu — bloku with, który zapewnia zamknięcie pliku nawet w przypadku wyjątku w trakcie odczytu. Menedżer kontekstu to obiekt definiujący, co się dzieje przy wejściu i wyjściu z bloku with; dla plików wyjście oznacza automatyczne wywołanie close(). Oto dlaczego ma to znaczenie w praktyce:
# ❌ 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 blockNa długo działających serwerach to nie jest akademicka kwestia — przeciekanie uchwytów plików ostatecznie powoduje OSError: [Errno 24] Too many open files. Instrukcja with nic nie kosztuje i całkowicie zapobiega tej klasie błędów. Używaj jej wszędzie.
Odczytywanie plików — cztery sposoby, po jednym właściwym narzędziu
Python daje kilka metod na obiekcie pliku, a wybór właściwej ma większe znaczenie niż przyznaje większość samouczków:
f.read()— odczytuje cały plik do jednego ciągu znaków. Odpowiednie dla małych plików konfiguracyjnych, niebezpieczne dla dużych.f.readline()— odczytuje jedną linię na raz, przesuwając wewnętrzny wskaźnik. Użyteczne gdy potrzebujesz ręcznej kontroli nad iteracją.f.readlines()— odczytuje wszystkie linie do listy. Wygodne, ale nadal ładuje cały plik do pamięci.for line in f:— protokół iteratora. Odczytuje jedną linię na raz bez ładowania pełnego pliku. To jest ten, po który należy domyślnie sięgać.
Oto realistyczny przykład: odczytywanie pliku konfiguracyjnego w stylu .env i przekształcanie go w słownik. To jest rodzaj rzeczy, które naprawdę piszesz, a nie wymyślona demonstracja „czytaj hello.txt":
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(): podczas odczytywania linii każda linia z wyjątkiem ostatniej zawiera końcowy znak \n (a w systemie Windows \r\n). Wywołaj line.strip(), aby usunąć oba. Jeśli chcesz usunąć tylko znak nowej linii, a nie wiodące białe znaki, użyj line.rstrip('\n').Zapisywanie i dołączanie — wiedz, który tryb niszczy dane
Drugi argument dla open() to tryb. Dwa tryby stale wprawiają ludzi w zakłopotanie:
'w'— tryb zapisu. Otwiera plik do zapisu. Jeśli plik już istnieje, jest natychmiast obcinany do zera bajtów — zanim napiszesz jeden znak. To jest ciche niszczenie danych, jeśli otworzysz niewłaściwą ścieżkę.'a'— tryb dołączania. Otwiera plik i przesuwa wskaźnik zapisu na koniec. Istniejąca zawartość nigdy nie jest naruszana. Nowe zapisy trafiają za to, co było już tam.
Dobrym przypadkiem użycia dla trybu dołączania jest zapisywanie ustrukturyzowanego pliku logów z znacznikami czasu. Oto wzorzec użyteczny zarówno w skryptach, jak i małych usługach:
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') tworzy plik, jeśli nie istnieje — co jest wygodne — ale też cicho niszczy plik, jeśli istnieje. Błędnie wpisana ścieżka może wymazać plik produkcyjny bez żadnego komunikatu o błędzie. Jeśli nie jesteś pewny, czy plik powinien zostać nadpisany, najpierw sprawdź za pomocą Path(path).exists() lub użyj trybu 'x', który zamiast nadpisywania zgłasza FileExistsError.Kodowanie — błąd, który w końcu ugryzie każdego
To jest jedyne najczęstsze źródło cichego uszkadzania danych w obsłudze plików Pythona. Domyślne kodowanie Pythona 3 podczas wywołania open() bez jego podania jest określane przez locale.getpreferredencoding() — co na Windows to zazwyczaj cp1252, a na Linux/macOS zazwyczaj UTF-8. Oznacza to, że kod, który działa doskonale na Macu, może po cichu uszkadzać lub wysypywać się na serwerze Windows, gdy plik zawiera jakikolwiek znak spoza ASCII. Rozwiązanie to jeden dodatkowy 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()Problem BOM jest szczególnie powszechny w plikach CSV eksportowanych z Microsoft Excel — plik zaczyna się ukrytym znakiem \ufeff, który pojawia się jako  przy odczycie z niewłaściwym kodowaniem lub powoduje, że pierwszy nagłówek kolumny wygląda jak name zamiast name. Użycie encoding='utf-8-sig' obsługuje to transparentnie. Zobacz dokumentację kodeków Pythona, aby uzyskać pełną listę nazw kodowań.
encoding='utf-8' (lub 'utf-8-sig' dla eksportów z Excela) do każdego wywołania open(). Zrób z tego nawyk — nic nie kosztuje i eliminuje całą kategorię błędów specyficznych dla środowiska.Praca ze ścieżkami — używaj pathlib
Stary sposób budowania ścieżek plików w Pythonie to konkatenacja ciągów znaków lub os.path.join(). Nowoczesny sposób to pathlib.Path, dostępny od Pythona 3.4 i w pełni dojrzały od 3.6. Obsługuje separatory ścieżek prawidłowo zarówno na Windows, jak i Unix bez myślenia o tym, i zastępuje garść wywołań os.path czytelnym dostępem do atrybutów.
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"Operator / nie jest tu dzieleniem — Path nadpisuje go tak, aby oznaczał łączenie ścieżek. Czyta się naturalnie i eliminuje problemy z cudzysłowami i separatorami, które pojawiają się przy budowaniu ścieżek opartych na ciągach. Jeszcze jedna użyteczna metoda: path.read_text(encoding='utf-8') to skrót dla wzorca otwierania/odczytu/zamykania, gdy chcesz po prostu zawartość pliku jako ciąg znaków.
Odczytywanie dużych plików bez wysadzania pamięci
Gdy plik jest mały — powiedzmy, poniżej kilku megabajtów — f.read() lub f.readlines() jest w porządku. Gdy jest to 500 MB log serwera lub wielogigabajtowy eksport danych, ładowanie całości do pamięci to szybka droga do MemoryError lub zakończenia procesu przez OS. Rozwiązaniem jest iteracja linia po linii:
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)}")Wzorzec for line in f: działa, ponieważ obiekt pliku Pythona implementuje protokół iteratora — pobiera linie z dysku po jednej za pomocą wewnętrznego bufora, więc użycie pamięci jest zasadniczo stałe niezależnie od rozmiaru pliku. Dla naprawdę ogromnych plików (dziesiątek gigabajtów), gdzie nawet iteracja linia po linii nie jest wystarczająco szybka, mmap pozwala mapować plik w pamięci i przeszukiwać go wyrażeniami regularnymi bez w ogóle go odczytywania — ale w większości przypadków iterator linii jest wszystkim, czego potrzebujesz.
Odczytywanie i zapisywanie JSON i CSV
Dwa formaty pojawiają się stale w prawdziwej pracy Pythona, a oba mają dedykowane moduły standardowej biblioteki, które prawidłowo obsługują cudzysłowy, escapowanie i strukturę — nie parsuj ich za pomocą podziałów ciągów.
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)Kilka rzeczy wartych odnotowania: przekaż newline='' podczas otwierania plików CSV — moduł csv obsługuje własne zakończenia linii, a pozwolenie na ingerencję trybu powszechnych nowych linii Pythona powoduje podwójne puste wiersze na Windows. W przypadku JSON, ensure_ascii=False pozwala na zapis znaków spoza ASCII (litery z akcentami, znaki CJK itp.) takimi, jakimi są, zamiast escapowania do sekwencji \uXXXX — znacznie bardziej czytelne wyjście. Jeśli pracujesz z danymi JSON lub CSV i chcesz je sprawdzić lub transformować wizualnie, Formater JSON i Formater CSV na tej stronie są dobrym uzupełnieniem podejścia kodowego.
Obsługa błędów — trzy wyjątki, które zobaczysz
Operacje na plikach kończą się niepowodzeniem w przewidywalny sposób. Jawna obsługa każdego przypadku daje komunikaty o błędach, które są naprawdę użyteczne zamiast ogólnego 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}"
)Cztery wyjątki obejmują prawie każdy rzeczywisty tryb awarii: plik nie istnieje, nie masz uprawnień, kodowanie jest błędne lub zawartość jest źle sformatowana. Każdy komunikat mówi następnemu programiście (lub Tobie o 2 w nocy) dokładnie, co poszło nie tak i gdzie szukać. Łapanie gołego Exception i drukowanie „coś poszło nie tak" to nie obsługa błędów — to tylko przenoszenie zamieszania w dół.
FileNotFoundError i zwrócenie None lub domyślnego słownika jest w porządku. Wybierz jedno zachowanie na funkcję i udokumentuj je.Podsumowanie
Krótka wersja wszystkiego powyżej: zawsze używaj bloków with, zawsze przekazuj encoding='utf-8', używaj pathlib.Path do konstruowania ścieżek i iteruj po liniach zamiast odczytywać całe pliki, gdy rozmiar jest nieznany. Te cztery nawyki eliminują zdecydowaną większość błędów w obsłudze plików przed dotarciem na produkcję.
Dla głębszego czytania: sekcja samouczka Pythona dotycząca odczytywania i zapisywania plików dokładnie omawia podstawy. Dokumentacja pathlib warta jest zakładki — to jedna z najbardziej użytecznych części standardowej biblioteki i większość programistów Pythona jej nie wykorzystuje. Dokumentacja modułu csv i dokumentacja modułu json mają dobre przykłady dla przypadków brzegowych (niestandardowe separatory, strumieniowy JSON itp.) wartych przeczytania, jeśli regularnie pracujesz z tymi formatami.