File Handling
Table of Contents
Why File Handling Matters for Backend Development
Backends constantly work with files. Even if you do not think about it, you are dealing with files when you:
- Read configuration from
.envor.yamlfiles - Log errors to a log file
- Store uploaded images or documents
- Import or export data as CSV or JSON
- Cache data on disk for later reuse
Understanding file handling at the programming fundamentals level prepares you for all these tasks, no matter which backend framework or language you use later.
In this chapter, you will focus on:
- How programs interact with the file system
- Reading and writing different kinds of files
- Safe patterns that avoid data loss and resource leaks
- Common pitfalls such as encoding issues and path problems
You will see examples in a generic pseudocode style, then recognize the same patterns easily in real backend languages like Python, JavaScript, or Go.
Key idea: File handling is about opening a file, doing something with it, and closing it correctly, while handling errors and avoiding data corruption.
Files and Paths
What Is a File?
A file is a named collection of data stored on a disk or other storage device.
Some important properties of a file:
- Has a name:
config.json,users.csv,error.log - Has a path: where it is located in the directory structure
- Has content: bytes that may represent text, images, video, etc
- Has metadata: size, creation date, permissions, etc
Backend code usually cares most about the path and the content.
Text files vs binary files
- Text files: contain human readable text, such as:
.txt,.log,.csv,.json,.html- Binary files: contain data not meant to be read directly as text:
.jpg,.png,.zip,.pdf,.mp4
You handle text and binary files slightly differently, especially regarding encoding.
Paths: Absolute and Relative
The path tells your program where a file lives.
Example directory structure:
/home/backend/app/
βββ main.py
βββ config/
β βββ settings.json
βββ logs/
βββ app.logAbsolute paths
Absolute paths start at the root of the file system.
Examples on Linux or macOS:
/home/backend/app/config/settings.json/var/log/app.log
Examples on Windows:
C:\Users\backend\app\config\settings.jsonD:\logs\app.log
They always point to the same place, no matter where your program starts, but they make your code less portable and harder to deploy across machines.
Relative paths
Relative paths start from the current working directory of your program.
If your current directory is /home/backend/app:
config/settings.jsonrefers to/home/backend/app/config/settings.jsonlogs/app.logrefers to/home/backend/app/logs/app.log
Relative paths make your code easier to move between environments, as long as your project structure is consistent.
Parent and current directory symbols
You will often see:
.current directory..parent directory
Examples:
./config/settings.json../shared/schema.sql
Path Handling in Code
You should avoid building paths by manual string concatenation such as:
path = "logs/" + filename
This can break on different operating systems, because directory separators differ, for example / on Linux and \ on Windows.
Most languages provide helpers to safely build paths.
Example in pseudocode:
path = join_path("logs", "app.log")
Under the hood, join_path chooses the correct separator for the current operating system.
Whenever you need to:
- Move up or down directories
- Join folder names with file names
prefer a built-in path utility. This becomes very important when you deploy your backend to Linux servers while developing on Windows or macOS.
Opening and Closing Files
Any file operation follows a simple life cycle:
- Open the file
- Read or write or append
- Close the file
If you forget to close a file, you can:
- Lock the file so that other parts of the application cannot use it
- Leak system resources such as file descriptors
- Corrupt data if the system crashes before data is fully written
File Modes
When you open a file, you must specify a mode that tells the system what you want to do.
Common modes:
| Mode | Purpose | File must exist? | Content preserved? |
|---|---|---|---|
r | Read text | Yes | Yes |
w | Write text, overwrite file | No | No, file is truncated to zero length |
a | Append text to end of file | No | Yes, new data added at the end |
r+ | Read and write, no truncation | Yes | Yes |
w+ | Read and write, overwrite file | No | No, content is cleared when opened |
b | Binary flag, combined with others | Depends | Depends, same as base mode |
Examples:
"rb": read binary"wb": write binary"ab": append binary
Many languages use a similar set of modes.
Very important rule:
Opening a file with w or w+ deletes its existing content immediately. Use it only when you are sure you want to replace everything.
Basic Open / Read / Close
Pseudocode for safely reading a file:
file = open("config/settings.json", mode="r")
content = file.read()
file.close()
This pattern works, but has a problem. If something goes wrong between open and close, for example:
- The file is removed
- An exception is thrown while reading
then file.close() might never be called.
Context Managers (Safe Pattern)
Most languages offer a construct that guarantees closing the file automatically. In Python this is with, in other languages you may see using, defer, or similar.
Pseudocode:
with open("logs/app.log", mode="a") as file:
file.write("Application started\n")
When the with block ends, the file is closed, even if an error occurs inside the block.
Always prefer this pattern in backend code, especially when you may open many files inside loops or request handlers.
Reading Files
Backend services read files to get configuration, templates, data seeds, or imports.
There are several reading strategies, depending on file size and use case.
Reading Entire Content
Use this only for relatively small files, for example configuration files.
with open("config/settings.json", "r") as file:
content = file.read()
print(content)
Now content holds the full file as a single string.
Example: Load a SQL seed script
with open("db/seed.sql", "r") as file:
seed_sql = file.read()
execute_sql(seed_sql)Reading Line by Line
When you have large text files, especially logs or CSV data, you should process them line by line to avoid loading the entire file into memory.
with open("logs/access.log", "r") as file:
for line in file:
process_log_line(line)
Here, process_log_line might:
- Parse IP address and URL
- Count requests per user
- Detect suspicious activity
You only have one line in memory at a time, which is efficient for large files.
Reading Fixed Size Chunks
Chunked reading is useful when dealing with binary files, such as large images or videos, or streaming files over HTTP.
chunk_size = 4096 # 4 KB
with open("videos/tutorial.mp4", "rb") as file:
while True:
chunk = file.read(chunk_size)
if not chunk:
break
send_over_network(chunk)Advantages:
- Memory friendly
- You can start streaming data before you have the whole file
Backend download endpoints often work internally with chunked reading.
Handling Encodings
Text files are stored as bytes, and encoding describes how characters map to bytes.
Common encodings:
UTF-8(modern standard, recommended)ISO-8859-1Windows-1252
If you read a UTF 8 encoded file with the wrong encoding, you might get:
- Strange characters
- Errors such as "invalid character"
Many languages let you specify encoding when opening the file:
with open("config/settings.json", "r", encoding="utf-8") as file:
content = file.read()For backend work, you should usually standardize on UTF 8.
Rule of thumb:
Unless you have a very specific reason, always use UTF 8 for reading and writing text files.
Writing and Appending to Files
Backends write to files in many situations:
- Creating or updating configuration files
- Writing log files
- Exporting reports as CSV or JSON
- Generating temporary files for background jobs
Writing Text Files
When you open a file in write mode, the previous content is erased.
with open("logs/app.log", "w") as file:
file.write("Application started\n")
file.write("Another log entry\n")
After this code, app.log contains exactly two lines. If it existed before, its old content is gone.
Example: Export user data as CSV
Imagine you have a list of users:
users = [
{"id": 1, "email": "alice@example.com"},
{"id": 2, "email": "bob@example.com"},
]You can export them:
with open("exports/users.csv", "w", encoding="utf-8") as file:
file.write("id,email\n")
for user in users:
line = user["id"] + "," + user["email"] + "\n"
file.write(line)
Resulting users.csv:
id,email
1,alice@example.com
2,bob@example.comAppending to Files
Appending is used heavily for logging and any situation where you want to add new data without losing old data.
with open("logs/app.log", "a", encoding="utf-8") as file:
file.write("2026-08-27 10:00:00 - User login: alice\n")Each time this code runs, a new line is added to the end of the file.
Example: Simple request logger
def log_request(method, path, status_code):
line = method + " " + path + " " + str(status_code) + "\n"
with open("logs/access.log", "a", encoding="utf-8") as file:
file.write(line)You could call this from your HTTP handler:
log_request("GET", "/users/1", 200)Writing Binary Files
For binary content, such as image uploads, use binary mode.
def save_uploaded_image(file_bytes, filename):
with open("uploads/" + filename, "wb") as file:
file.write(file_bytes)
Here file_bytes is a sequence of bytes from a user upload.
Overwriting vs Updating
Sometimes you need to change a file in place, for example update one line.
A simple pattern:
- Read original file line by line
- Modify each line as needed
- Write to a temporary file
- Replace original file with temporary file
Example in pseudocode:
with open("data/users.txt", "r") as source, open("data/users.tmp", "w") as target:
for line in source:
updated_line = transform(line)
target.write(updated_line)
rename("data/users.tmp", "data/users.txt")This pattern reduces the risk of corrupting your original file if something goes wrong.
Working with CSV and JSON Files
You will frequently see CSV and JSON in backend development.
CSV: Comma Separated Values
CSV files represent rows of data, often used for imports and exports to spreadsheets.
Example users.csv:
id,email,age
1,alice@example.com,30
2,bob@example.com,25To read:
with open("users.csv", "r", encoding="utf-8") as file:
header = file.readline().strip()
for line in file:
fields = line.strip().split(",")
user_id = fields[0]
email = fields[1]
age = fields[2]
save_user(user_id, email, age)In real code, you will use a CSV library that handles:
- Quoted fields
- Commas inside values
- Missing values
but the basic idea remains the same.
To write:
with open("export.csv", "w", encoding="utf-8") as file:
file.write("id,email,age\n")
for user in users:
line = user.id + "," + user.email + "," + str(user.age) + "\n"
file.write(line)JSON: JavaScript Object Notation
JSON is the most common format for APIs and configuration in modern backends.
Example config.json:
{
"database": {
"host": "localhost",
"port": 5432
},
"debug": true
}To read JSON, you usually:
- Read the file as text
- Parse the JSON string into a data structure (dictionary / object)
with open("config.json", "r", encoding="utf-8") as file:
text = file.read()
config = parse_json(text)
db_host = config["database"]["host"]
db_port = config["database"]["port"]To write JSON:
config = {
"database": {
"host": "db.example.com",
"port": 5432
},
"debug": false
}
text = to_json(config, indent=2)
with open("config.json", "w", encoding="utf-8") as file:
file.write(text)
Here indent=2 pretty prints the JSON for humans, which is useful for configuration and logs.
File System Operations
Backend services often need to manage directories and check for file existence.
Checking If a File Exists
Before reading or writing, you might want to test whether a file is present.
if file_exists("config/settings.json"):
with open("config/settings.json", "r") as file:
content = file.read()
else:
print("Settings file missing")This avoids errors like "No such file or directory".
Creating Directories
If your backend writes files to a directory that may not exist yet, create the directory first.
if not dir_exists("logs"):
create_directory("logs")
with open("logs/app.log", "a") as file:
file.write("Server started\n")
Often there is a function to create nested directories in one call, such as create_all_directories("data/imports/2026").
Listing Files
Sometimes you need to read all files from a directory, for example process all uploaded files that are waiting for a background job.
files = list_files("uploads")
for filename in files:
process_file("uploads/" + filename)You can filter by extension:
files = list_files("uploads")
for filename in files:
if filename.endswith(".jpg") or filename.endswith(".png"):
process_image("uploads/" + filename)Renaming and Deleting
Renaming:
rename("logs/app.log", "logs/app-old.log")Deleting:
delete_file("logs/app-old.log")Use these carefully in backend code, especially in production. Deleting the wrong file can be very costly.
Error Handling in File Operations
File operations fail for many reasons:
- File does not exist
- Insufficient permissions
- Disk is full
- File is locked by another process
- Path is invalid
- Network issues if using remote file systems
You must always assume that file operations can fail and handle errors gracefully.
Typical Error Handling Pattern
Basic idea in pseudocode:
try:
with open("config/settings.json", "r") as file:
content = file.read()
config = parse_config(content)
except FileNotFoundError:
log_error("Config file not found")
use_default_config()
except PermissionError:
log_error("No permission to read config file")
stop_application()
except Exception as e:
log_error("Unexpected error: " + str(e))
stop_application()For backend configuration, failing fast with a clear error is often better than running with unknown settings.
Validating Data after Reading
Even if the file is read successfully, its content might be invalid:
- Config values missing
- Wrong data types
- File partially written by a previous crash
Always validate the content after reading.
Example:
config = parse_json(content)
if "database" not in config or "host" not in config["database"]:
log_error("Invalid config: database.host missing")
stop_application()File Handling Best Practices for Backends
To summarize and highlight what matters most for backend development:
Critical file handling rules:
- Always use a context manager (or equivalent) so files are closed automatically.
- Be very careful with
wandw+; they erase existing content immediately. - Use UTF 8 encoding for text files unless you have a strong reason not to.
- Do not build paths with plain string concatenation. Use path utilities.
- Prefer line by line or chunked reading for large files.
- Always handle possible errors: missing files, permissions, corrupted data.
- Validate the data you read, especially for configuration and imports.
- Do not store sensitive secrets in plain text files without protection.
Simple Backend Style Examples
To connect everything to backend scenarios, here are some small, realistic examples.
Loading Configuration with Fallback
DEFAULT_CONFIG = {
"db_host": "localhost",
"db_port": 5432,
"debug": false
}
def load_config():
if not file_exists("config.json"):
return DEFAULT_CONFIG
try:
with open("config.json", "r", encoding="utf-8") as file:
text = file.read()
config = parse_json(text)
return merge(DEFAULT_CONFIG, config)
except Exception as e:
log_error("Failed to load config.json: " + str(e))
return DEFAULT_CONFIGThis code:
- Uses defaults if the file is missing or invalid
- Logs an error but keeps the service running
Rotating a Log File
Simple manual rotation pattern:
MAX_LOG_SIZE = 10 * 1024 * 1024 # 10 MB
def rotate_log_if_needed():
size = file_size("logs/app.log")
if size > MAX_LOG_SIZE:
rename("logs/app.log", "logs/app.log.bak")
def log_message(message):
rotate_log_if_needed()
with open("logs/app.log", "a", encoding="utf-8") as file:
file.write(message + "\n")Real applications often use built in log rotation tools, but the pattern is based on file handling operations you already know.
Importing Data from an Upload Directory
def import_pending_files():
if not dir_exists("imports"):
return
files = list_files("imports")
for filename in files:
full_path = join_path("imports", filename)
try:
with open(full_path, "r", encoding="utf-8") as file:
for line in file:
process_import_line(line)
rename(full_path, join_path("imports", "done_" + filename))
except Exception as e:
log_error("Failed to import " + filename + ": " + str(e))
rename(full_path, join_path("imports", "error_" + filename))This shows a realistic pattern:
- List files
- Process each file line by line
- Move successful files to a "done" state
- Move failed files to an "error" state
All based on file handling fundamentals.
Understanding these core file handling concepts prepares you for many backend tasks, from configuration and logging to data import and export. In later chapters, you will see how frameworks and libraries build on these basics to offer higher level abstractions, but the underlying principles remain the same.
Views: 8
KAHIBARO