π Working with Files and JSON
Reading a config file, writing a log, caching an API response to disk β backends do this constantly, and JSON is the lingua franca they use to do it. This lesson lines up file I/O and JSON handling in Node.js, Python, and PHP so you can see that the steps are identical: open, transfer bytes, decode or encode, and handle the things that can go wrong.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Read and write text files in Node.js, Python, and PHP
- Parse JSON into native data and serialize native data back to JSON in each language
- Explain text vs binary mode and why UTF-8 encoding matters
- Handle the common error cases β missing files, permission denied, malformed JSON
- Combine the two into a real task: load, modify, and save a JSON data file safely
Estimated Time: 30β40 minutes β’ Difficulty: Intermediate
Hands-on: Build a tiny "visit counter" that reads a JSON file, increments a number, and writes it back β in all three languages.
In This Lesson
The Shape of File I/O
A file on disk is just a sequence of bytes. To turn those bytes into something useful your program does two steps in a row: it reads the raw bytes into a string, then it decodes that string into a structure (often via JSON). Writing runs the same pipeline in reverse.
π Key Terms
Serialize (stringify / encode): turn an in-memory object into a text form that can be stored or sent.
Deserialize (parse / decode): turn stored text back into an in-memory object.
Encoding: the rule (almost always UTF-8) mapping characters to bytes.
Reading a Text File
The simplest case: read an entire text file into a string. Note that Node's file API is asynchronous by default β you await it β while Python and PHP read synchronously.
// Node.js β the modern promise-based API
import { readFile } from 'node:fs/promises';
const text = await readFile('notes.txt', 'utf8');
console.log(text);
# Python β open() with a context manager (auto-closes)
with open('notes.txt', 'r', encoding='utf-8') as f:
text = f.read()
print(text)
<?php
// PHP β one call reads the whole file into a string
$text = file_get_contents('notes.txt');
echo $text;
β οΈ Always specify the encoding
If you omit 'utf8' in Node's readFile, you get a raw Buffer of bytes, not a string. In Python, omitting encoding='utf-8' falls back to the platform default, which can mangle non-ASCII text on some systems. Be explicit β UTF-8 is the safe, portable choice.
Reading line by line
For large files you often don't want the whole thing in memory at once:
# Python β iterate a file object line by line, lazily
with open('big.log', 'r', encoding='utf-8') as f:
for line in f:
process(line.rstrip('\n'))
<?php
// PHP β file() returns an array of lines
foreach (file('big.log', FILE_IGNORE_NEW_LINES) as $line) {
process($line);
}
Writing a Text File
Writing replaces the file's contents (or creates it if absent). A separate append mode adds to the end instead.
// Node.js
import { writeFile, appendFile } from 'node:fs/promises';
await writeFile('out.txt', 'Hello\n', 'utf8'); // overwrite / create
await appendFile('out.txt', 'Another line\n'); // add to the end
# Python β mode 'w' overwrites, 'a' appends
with open('out.txt', 'w', encoding='utf-8') as f:
f.write('Hello\n')
with open('out.txt', 'a', encoding='utf-8') as f:
f.write('Another line\n')
<?php
// PHP β file_put_contents overwrites; add FILE_APPEND to append
file_put_contents('out.txt', "Hello\n");
file_put_contents('out.txt', "Another line\n", FILE_APPEND);
| Task | Node.js | Python | PHP |
|---|---|---|---|
| Read whole file | readFile(p, 'utf8') | open(p).read() | file_get_contents(p) |
| Write (overwrite) | writeFile(p, s) | open(p, 'w') | file_put_contents(p, s) |
| Append | appendFile(p, s) | open(p, 'a') | β¦, FILE_APPEND |
| Sync or async? | Async (await) | Sync | Sync |
π‘ Node also has a sync flavour
Node offers readFileSync / writeFileSync for scripts and startup code where blocking is fine. Inside a request handler, though, prefer the fs/promises versions so one slow disk read doesn't freeze the whole server.
Parsing & Serializing JSON
JSON is just a string in a specific format. Parsing turns that string into a native structure; serializing turns a native structure back into a string. This step is entirely separate from touching the disk.
Parse: text β object
// Node.js
const obj = JSON.parse('{"name":"Ada","tags":["a","b"]}');
obj.name; // 'Ada' -> a normal JS object
# Python
import json
obj = json.loads('{"name":"Ada","tags":["a","b"]}')
obj['name'] # 'Ada' -> a dict
<?php
// PHP β pass true to get an associative array (else you get stdClass)
$obj = json_decode('{"name":"Ada","tags":["a","b"]}', true);
$obj['name']; // 'Ada'
Serialize: object β text
// Node.js β the 3rd arg is indent width for pretty output
const data = { name: 'Ada', tags: ['a', 'b'] };
JSON.stringify(data); // '{"name":"Ada","tags":["a","b"]}'
JSON.stringify(data, null, 2); // pretty, 2-space indent
# Python β indent= for pretty; ensure_ascii=False keeps Γ©, δΈ as-is
json.dumps(data) # compact
json.dumps(data, indent=2, ensure_ascii=False) # pretty + unicode
<?php
// PHP β flags control formatting
json_encode($data); // compact
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
β οΈ JSON is stricter than it looks
Real JSON requires double quotes on keys and strings, forbids trailing commas, and has no comments. A single quote or a stray comma makes JSON.parse throw a SyntaxError, json.loads raise JSONDecodeError, and json_decode return null. Never trust JSON from a user or network without wrapping the parse in error handling.
π How types map
JSON object β JS object / Python dict / PHP array. JSON array β all three's list type. JSON null β null / None / null. Note JSON has no date type β dates travel as strings and you re-parse them yourself.
The ReadβModifyβWrite Round Trip
Combine the two halves and you get the everyday pattern: load a JSON file into an object, change something, and write it back. Here it is end-to-end in each language, with the parse guarded against a malformed or missing file.
// Node.js β bump a counter stored in data.json
import { readFile, writeFile } from 'node:fs/promises';
let data;
try {
data = JSON.parse(await readFile('data.json', 'utf8'));
} catch (err) {
data = { count: 0 }; // file missing or invalid -> start fresh
}
data.count += 1;
await writeFile('data.json', JSON.stringify(data, null, 2), 'utf8');
# Python
import json
try:
with open('data.json', 'r', encoding='utf-8') as f:
data = json.load(f) # load() reads + parses in one step
except (FileNotFoundError, json.JSONDecodeError):
data = {'count': 0}
data['count'] += 1
with open('data.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2) # dump() serializes + writes
<?php
// PHP
$data = ['count' => 0];
if (is_readable('data.json')) {
$decoded = json_decode(file_get_contents('data.json'), true);
if (is_array($decoded)) {
$data = $decoded; // guard against null on bad JSON
}
}
$data['count'] = ($data['count'] ?? 0) + 1;
file_put_contents('data.json', json_encode($data, JSON_PRETTY_PRINT));
β Note the shortcuts
Python's json.load(f) / json.dump(data, f) fuse the file step and the JSON step into one call β handy, but under the hood it's still read-then-parse. Node and PHP keep the two steps visible, which makes the pipeline easy to see.
Encoding, Binary & Error Cases
Text vs binary
Text mode decodes bytes into characters using an encoding. Binary mode hands you the raw bytes untouched β use it for images, PDFs, or anything that isn't human-readable text.
# Python β 'rb' = read bytes; no encoding argument in binary mode
with open('logo.png', 'rb') as f:
raw = f.read() # a bytes object, not a str
// Node.js β omit the encoding to get a Buffer of raw bytes
import { readFile } from 'node:fs/promises';
const raw = await readFile('logo.png'); // a Buffer
Common error cases to handle
| Situation | Node.js | Python | PHP |
|---|---|---|---|
| File not found | throws ENOENT | FileNotFoundError | warning + false |
| No permission | throws EACCES | PermissionError | warning + false |
| Malformed JSON | SyntaxError | JSONDecodeError | returns null |
β οΈ PHP fails quietly β check the return value
Unlike Node and Python, PHP's file_get_contents returns false (with a warning) on failure rather than throwing, and json_decode returns null on bad JSON. Always test the result: use is_readable() before reading, check for false, and confirm json_decode didn't return null (or pass the JSON_THROW_ON_ERROR flag to make it throw like the others).
<?php
// PHP β make json_decode throw instead of returning null
try {
$data = json_decode($text, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
// now handled like Python / Node
}
π‘ Safe-write tip: To avoid a half-written file if the process dies mid-write, write to a temp file first and then rename it over the target. A rename is atomic on most filesystems, so readers never see a partial file.
Hands-on Exercise
ποΈ Build a Visit Counter
Objective: Write a small program that maintains a visits.json file shaped like { "total": 0, "lastVisit": null }. On each run it should:
- Read and parse
visits.jsonβ but start from a default object if the file is missing or invalid. - Increment
totalby 1 and setlastVisitto the current timestamp. - Write the updated object back as pretty-printed JSON.
- Print the new total.
Implement it in one language of your choice first, then try a second for contrast.
π‘ Hint
Reuse the readβmodifyβwrite pattern from the round-trip section. The only additions are the timestamp (new Date().toISOString() in JS, datetime.now().isoformat() in Python, date('c') in PHP) and printing the total at the end. Wrap the read+parse in your error handling so the very first run β when the file doesn't exist yet β still works.
β Solution
// Node.js β visits.js (run with: node visits.js)
import { readFile, writeFile } from 'node:fs/promises';
let data;
try {
data = JSON.parse(await readFile('visits.json', 'utf8'));
} catch {
data = { total: 0, lastVisit: null };
}
data.total += 1;
data.lastVisit = new Date().toISOString();
await writeFile('visits.json', JSON.stringify(data, null, 2), 'utf8');
console.log(`Total visits: ${data.total}`);
# Python β visits.py (run with: python visits.py)
import json
from datetime import datetime
try:
with open('visits.json', 'r', encoding='utf-8') as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
data = {'total': 0, 'lastVisit': None}
data['total'] += 1
data['lastVisit'] = datetime.now().isoformat()
with open('visits.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
print(f"Total visits: {data['total']}")
<?php
// PHP β visits.php (run with: php visits.php)
$data = ['total' => 0, 'lastVisit' => null];
if (is_readable('visits.json')) {
$decoded = json_decode(file_get_contents('visits.json'), true);
if (is_array($decoded)) {
$data = $decoded;
}
}
$data['total'] = ($data['total'] ?? 0) + 1;
$data['lastVisit'] = date('c');
file_put_contents('visits.json', json_encode($data, JSON_PRETTY_PRINT));
echo "Total visits: {$data['total']}\n";
π― Quick Quiz
Question 1: Which pair of Node.js calls turns a JSON file into a usable object?
Question 2: In PHP, what does json_decode return when given malformed JSON (without the throw flag)?
Question 3: Why should you pass 'utf8' to Node's readFile (or encoding='utf-8' to Python's open) when reading text?
Summary & Quiz
π Key Takeaways
- File work is two hops: bytes β text (the file API) and text β object (the JSON API) β keep them distinct in your head.
- Node uses
fs/promises(async,await) withJSON.parse/stringify; Python usesopenwith thejsonmodule; PHP usesfile_get_contents/file_put_contentswithjson_decode/encode. - Always be explicit about UTF-8 encoding, and use binary mode for non-text files.
- Handle the failure cases: Node and Python throw, but PHP returns
false/nullunless you passJSON_THROW_ON_ERROR. - The workhorse pattern is read β parse β modify β stringify β write, guarded so a missing file just starts fresh.
π Further Reading
π What's Next?
You now have every language-level building block β data structures, error handling, and file/JSON I/O. Next you'll put them together in the Weekend Project: A Polyglot Mini-Server, building the same small server in each language to feel their differences first-hand.
π Nice work!
You can read, transform, and persist data in three languages. Time to build something real.