File handling in Python is the process of creating, opening, reading, writing, appending, and closing files using built-in functions. You open a file with open(), work on it with methods such as read(), readline(), and write(), and close it safely with the with statement. The mode you pick ('r', 'w', 'a', 'x', 'b', '+') decides what your program is allowed to do with the file.
Every useful program eventually needs to remember something. A temperature logger has to keep yesterday’s readings, a gateway has to store its Wi-Fi settings, and a test script has to save its results. Variables live in RAM and disappear the moment power goes off, so the data needs a permanent home. That is exactly the job of file handling.
If you want a clear explanation of file handling in Python, this guide covers it in small practical steps, with code you can run today and with embedded and IoT examples such as sensor logging on a Raspberry Pi or a MicroPython board.
What you will get from this guide:
File handling is how a Python program talks to files stored on a disk, SD card, or flash memory. Python gives you a file object, also called a file handle, that works like a pipe between your code and the file.
In simple words, you tell Python which file to use, what you want to do with it, and when you are finished.
Think of RAM as a whiteboard and a file as a notebook. The whiteboard is fast but gets wiped when the power goes off. The notebook is slower but keeps everything you wrote.
| Feature | Text File | Binary File |
|---|---|---|
| Content | Human-readable characters | Raw bytes |
| Open with | 'r', 'w', 'a' | 'rb', 'wb', 'ab' |
| Python type | str | bytes |
| Typical examples | .txt, .csv, .json, .log | .bin, .dat, images, firmware images |
| Newline handling | Translated by Python | Never translated |
Rule of thumb: Choose text for data people will read, and binary for compact or byte-exact data such as sensor frames.
If you have written C code with FILE *, Python will feel familiar, with far less manual bookkeeping.
| Task | C | Python |
|---|---|---|
| Open a file | fopen("log.txt", "r") | open('log.txt', 'r') |
| Read a line | fgets(buf, size, fp) | f.readline() |
| Read everything | fread(buf, 1, size, fp) | f.read() |
| Write text | fputs(text, fp) | f.write(text) |
| Move position | fseek(fp, off, SEEK_SET) | f.seek(off, 0) |
| Flush buffer | fflush(fp) | f.flush() |
| Close | fclose(fp) | f.close() or a with block |
| Error check | Test for NULL and errno | try / except OSError |
The biggest win is the with statement. It behaves like RAII in C++: the file is released when the block ends, so you cannot leak a file descriptor by forgetting fclose().
open() asks the operating system for access and returns a file object.flush() pushes the buffer to the operating system, and os.fsync() asks the OS to write it to storage.close() flushes the buffer and releases the file descriptor.Work through these steps in order and you will have the complete workflow covered.
file_object = open(file_path, mode='r', encoding='utf-8')'r'.'utf-8', so your code behaves the same on Windows, Linux, and a Raspberry Pi.If you open a missing file for reading, Python raises FileNotFoundError.
| Mode | Purpose | If the File Exists | If the File Is Missing |
|---|---|---|---|
'r' | Read only (default) | Opens at the start | FileNotFoundError |
'w' | Write only | Erases old content | Creates it |
'a' | Append only | Adds data at the end | Creates it |
'x' | Exclusive create | FileExistsError | Creates it |
'r+' | Read and write | Keeps content, starts at the beginning | FileNotFoundError |
'w+' | Write and read | Erases old content | Creates it |
'a+' | Append and read | Keeps content, writes go to the end | Creates it |
'b' | Binary flag: 'rb', 'wb', 'ab' | Works with bytes, not text | Same as the base mode |
Watch out: 'w' is the most common cause of accidental data loss. If you only want to add data, use 'a'.
with open('report.txt', 'w', encoding='utf-8') as f:
f.write('Device: ESP32\n')
f.write('Status: Online\n')
f.writelines(['Temp: 27.4 C\n', 'Humidity: 61 %\n'])write() and writelines() do not add newline characters, so add \n yourself.str only, and binary mode accepts bytes only.with open('report.txt', 'a', encoding='utf-8') as f:
f.write('Uptime: 3 hours\n')with open('report.txt', 'r', encoding='utf-8') as f:
text = f.read() # whole file as one string
print(text)read() returns everything, and read(n) returns the next n characters.readline() returns one line each time you call it.readlines() returns a list of all lines, so use it only on small files.Looping over the file object is best for big files because it reads one line at a time:
with open('report.txt', 'r', encoding='utf-8') as f:
for line in f:
print(line.strip())Pro tip: line.strip() removes the trailing newline, so your output does not show blank lines between entries.
# Manual way: works, but easy to get wrong
f = open('data.txt', 'r', encoding='utf-8')
try:
data = f.read()
finally:
f.close()
# Recommended way: the file closes automatically
with open('data.txt', 'r', encoding='utf-8') as f:
data = f.read()close() flushes the buffer and frees the file descriptor.with block closes the file even if an exception occurs inside it.with open('a.txt') as src, open('b.txt', 'w') as dst:with open('data.bin', 'wb') as f:
f.write(bytes(range(10))) # bytes 0 to 9
with open('data.bin', 'rb') as f:
f.seek(4) # jump to byte 4
chunk = f.read(2) # reads bytes 4 and 5
print(list(chunk), f.tell()) # [4, 5] 6tell() returns the current position.seek(offset, whence) moves it: 0 is the start, 1 is the current position, and 2 is the end.fseek in C, use binary mode, because text mode allows only limited offsets.try:
with open('config.json', 'r', encoding='utf-8') as f:
data = f.read()
except FileNotFoundError:
print('Config file is missing, loading defaults.')
except PermissionError:
print('No permission to read the file.')
except OSError as err:
print(f'File error: {err}')FileNotFoundError and PermissionError are both subclasses of OSError.Reading and writing plain text is only the start. These are the patterns you will reach for again and again.
import csv
from datetime import datetime
def log_reading(temp_c, humidity):
with open('readings.csv', 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([datetime.now().isoformat(), temp_c, humidity])
log_reading(27.4, 61)newline='' stops the csv module from adding blank rows on Windows.csv.writer takes care of commas and quotes inside values.csv.reader or csv.DictReader to read the data back.import json
settings = {'device_id': 'node-07', 'sample_rate_hz': 10}
with open('config.json', 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=4)
with open('config.json', 'r', encoding='utf-8') as f:
loaded = json.load(f)import struct
FRAME = '<Hhf' # uint16 id, int16 temp x100, float32 volts
size = struct.calcsize(FRAME) # 8 bytes
with open('frames.bin', 'ab') as f:
f.write(struct.pack(FRAME, 7, 2740, 3.29))
with open('frames.bin', 'rb') as f:
node_id, temp_raw, volts = struct.unpack(FRAME, f.read(size))
print(node_id, temp_raw / 100, round(volts, 2))n * 8 and seek() can jump straight to it.< prefix fixes the byte order and removes padding, so the layout matches a packed C struct.fwrite() on a struct, with the layout spelled out explicitly.from pathlib import Path
log_dir = Path('logs')
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / 'boot.log'
log_file.write_text('System started\n', encoding='utf-8')
print(log_file.read_text(encoding='utf-8'))
print(log_file.exists())/ operator joins paths safely on Windows, Linux, and macOS.exists(), is_file(), and iterdir() replace many os.path calls.write_text() replaces the file content, so treat it like mode 'w'.os.rename('old.txt', 'new.txt') # Renames a file
shutil.copy('a.txt', 'b.txt') # Copies a file
os.remove('a.txt') # Deletes a file
os.listdir('.') # Lists the contents of a folderTime to combine everything. This script creates a log folder, writes a CSV header once, and appends a new reading every second.
import csv
import random
import time
from pathlib import Path
LOG_FILE = Path('logs/sensor_log.csv')
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
def read_sensor():
# Replace this with a real sensor read on your board
return round(random.uniform(24.0, 30.0), 1), random.randint(55, 70)
is_new_file = not LOG_FILE.exists()
with open(LOG_FILE, 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
if is_new_file:
writer.writerow(['timestamp', 'temp_c', 'humidity_pct'])
for _ in range(5):
temp, hum = read_sensor()
writer.writerow([time.strftime('%Y-%m-%d %H:%M:%S'), temp, hum])
f.flush()
time.sleep(1)pathlib creates the folder and checks whether the file already exists.'a' keeps old readings every time you run the script.flush() after every row keeps data loss small if the power drops.read_sensor() for a DHT22 or BME280 reading on a Raspberry Pi.File handling looks the same on a laptop and on a small board, but the stakes are higher. Storage is limited, power can vanish at any moment, and flash memory wears out. A classic field failure is a logger that works perfectly on the desk and corrupts its file the first time someone unplugs it.
open(), read(), write(), and os.listdir() work on the board’s flash file system.Writes sit in buffers first. If power drops before the data reaches storage, it is gone.
import os
with open('readings.csv', 'a', encoding='utf-8') as f:
f.write('2026-10-17T10:30:00,27.4,61\n')
f.flush() # push the Python buffer to the OS
os.fsync(f.fileno()) # ask the OS to write it to storageFor files that must never be half-written, such as configuration, write to a temporary file and then swap it in:
import json
import os
def save_config(path, data):
tmp = path + '.tmp'
with open(tmp, 'w', encoding='utf-8') as f:
json.dump(data, f)
f.flush()
os.fsync(f.fileno())
os.replace(tmp, path) # atomic swap on Linux: the old file stays valid until this lineIf power fails before os.replace(), the old configuration is still intact.
On MicroPython, call f.flush() and close files promptly, because its os module is a reduced version of the CPython one.
os.statvfs('/') reports block size and free blocks.File handling is the right tool when your data is small, local, and simple. It is the wrong tool when many users or devices need to read and write the same data at the same time.
| Situation | Better Choice | Why |
|---|---|---|
| Many users or processes write at once | SQLite or a database server | Handles locking and transactions |
| You need to search or filter large data | A database | Indexed queries beat scanning files |
| Live data for many consumers | MQTT or a message broker | Built for real-time streaming |
| Data shared across devices | Cloud storage or an API | Centralized access |
| Tiny settings on a microcontroller | EEPROM, FRAM, or ESP32 NVS | Made for small, frequent, reliable writes |
SQLite is worth remembering: Python ships with the sqlite3 module, and the whole database lives in a single file.
| Method | What It Does |
|---|---|
open() | Opens a file and returns a file object |
read(n) | Reads the whole file, or n characters or bytes |
readline() | Reads one line |
readlines() | Returns all lines as a list |
write(s) | Writes a string, or bytes in binary mode |
writelines(seq) | Writes a sequence of strings without adding newlines |
seek(offset, whence) | Moves the file position |
tell() | Returns the current position |
flush() | Pushes buffered data to the operating system |
close() | Flushes and releases the file |
with block every time.'w' by accident: It erases the file, so use 'a' to add data or 'x' to refuse overwriting.encoding='utf-8' so behavior matches across systems.read() or readlines(): Loop over the file object instead.write() does not add \n.'wb' expects bytes, not str.pathlib, because a backslash in a normal string acts as an escape character.try / except.flush() and os.fsync() for critical data.with statement handles the closing for you.'w' erases existing content.struct, and pathlib cover most real-world needs.Ready to take Python beyond scripts and into real hardware? Combine these file handling skills with C, microcontrollers, and IoT projects through the hands-on embedded systems programs at IIES, Bangalore.
File handling in Python is the ability to create, open, read, write, append, and close files from a program so data survives after the program ends. Python provides the built-in open() function and file objects with methods such as read(), write(), and close().
The main modes are ‘r’ (read), ‘w’ (write and overwrite), ‘a’ (append), and ‘x’ (create only if the file does not exist). Add ‘+’ for reading and writing together, and ‘b’ for binary mode. Combine them, for example ‘rb’ or ‘a+’.
The with statement closes the file automatically, even if an error occurs. This prevents data loss, leaked file descriptors, and locked files.
read() returns the whole file as one string, readline() returns a single line per call, and readlines() returns a list of all lines. For large files, loop with for line in f so Python reads one line at a time.
Yes. MicroPython supports open(), read(), write(), and many os functions on the board’s flash file system or on an SD card. Keep files small, write in batches, and avoid constant writes to extend flash life.
Indian Institute of Embedded Systems – IIES