5 · File handling and log parsing Code
with open, line streaming, split vs csv, json, validation, aggregation, two-file joins · the NPE signature question and its follow-ups
Why it matters for NPE. The official guide names file handling first, and 7 of 22 firsthand reports describe a file task: join two CSVs, count word frequency, parse a delimited file too big for RAM. The follow-ups are always the same: complexity, dictionary size, missing rows or columns, try/except, and 'what if the file is 20 GB'.
Primer: the shape of every file question
Read a file line by line. Clean each line. Split it into fields. Validate. Update a small amount of state (usually a dictionary keyed by the thing you're asked about). Produce a sorted report. Then survive the follow-ups: complexity, how big the dictionary gets, a missing row or column, malformed lines, and "what if the file doesn't fit in memory". Firsthand reports describe exactly this pattern: join two CSVs on a key and sort by a computed value, count word frequency in a file, read a %-delimited fortune file and handle one bigger than RAM.
from collections import defaultdict, Counter
def report(path):
counts = Counter()
bad = [] # (line_no, reason)
with open(path, encoding='utf-8') as f: # streams; nothing loaded at once
for line_no, line in enumerate(f, 1):
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.split(',')
if len(parts) != 2:
bad.append((line_no, 'field count'))
continue
host, status = (p.strip() for p in parts)
if status.upper() == 'DOWN':
counts[host.lower()] += 1
return counts, bad
Watch
3:38 context managers, 5:46 reading, 8:57 chunks and seek, 12:41 file position, 14:44 writing. Skip the image-copy section after 20:25.
The full NPE motion end to end on a real log: 4:37 spot the line pattern, 7:03 write a regex with groups, 8:43 build the parser.
csv.reader vs DictReader (4:16 header handling), delimiters, 12:23 DictWriter. Why split(',') breaks on quoted fields.
3:49 file processing with generators, 8:05 generator pipelines: the "file bigger than RAM" answer in code.
0:30 Counter, 8:17 defaultdict, 10:34 deque. The three containers every parsing script uses.
0:40-10:59 is the standard-library part (loads/dumps, load/dump, indent). Watch before lab 3 and topic 15.
Lookup video, not a sit-through: groups, named groups, finditer, compile.
The file object
| Call | Returns | Memory | Use when |
|---|---|---|---|
for line in f | one line at a time, newline kept | O(longest line) | default for any line-oriented file |
f.read() | whole file as one str | O(file) | small config files only |
f.readlines() | list of all lines | O(file) | almost never; say why not |
f.read(n) | next n characters/bytes | O(n) | records not separated by newlines (the fortune file), binary data |
f.tell() / f.seek(pos) | current byte offset / jump to one | O(1) | index a huge file once, then random access |
open(path, 'w'|'a') | writer | stream the report out when it could be big; newline='' with the csv module |
Always with open(...): the file closes even on an exception. Say encoding='utf-8' and, if asked about garbage bytes, errors='replace'. Open in 'rb' when you need byte offsets that survive multi-byte characters.
Cleaning and splitting
line.rstrip('\n') # just the newline; keeps leading/trailing spaces in data
line.strip() # all surrounding whitespace, the usual choice
line.split() # any run of whitespace, empties dropped: good for log lines
line.split(',') # exact delimiter, empties kept: good for CSV without quotes
line.split(',', 2) # at most 2 splits: message fields that contain commas
key, _, value = line.partition('=') # first '=' only; always 3 parts
import csv
with open(path, newline='') as f:
for row in csv.DictReader(f): # handles quoted commas, header → keys
row['bytes'] # str! convert yourself
import json
rec = json.loads(line) # JSON Lines: one object per line
data = json.load(f) # one document: loads it all
import re
m = re.match(r'(\S+) (\S+) \[(.*?)\] "(\w+) (\S+)', line) # only when split() can't
Validation and the exception policy
Decide out loud what "bad input" means and what you do with it. Three acceptable policies: skip and count (default for reports), skip and record the line number (so someone can fix the data), or fail fast (for config files where silence is dangerous). Pick one, say it, move on.
try:
n = int(fields[1])
except (ValueError, IndexError):
bad.append(line_no)
continue
if n < 0:
bad.append(line_no); continue
Catch specific exceptions. A bare except: hides KeyboardInterrupt and real bugs, and interviewers notice. Check types too: '5' == 5 is False, and None from dict.get will crash int().
Aggregation patterns
| Ask | State | Code | Space |
|---|---|---|---|
| count per key | Counter | c[host] += 1 | O(u) distinct keys |
| sum per key | defaultdict(int) | total[iface] += n | O(u) |
| group rows per key | defaultdict(list) | by_region[r].append(row) | O(n): you kept everything |
| latest record per key | dict | if ts > latest.get(h, (-1,))[0]: latest[h] = (ts, status) | O(u) |
| distinct values per key | defaultdict(set) | ips_by_user[u].add(ip) | O(distinct pairs) |
| per-key streak / previous state | dict of small tuples | run[h] = run.get(h, 0) + 1 if status == 'DOWN' else 0 | O(u) |
| join two files | index the smaller file in a dict, stream the larger | region = inv.get(host, 'UNKNOWN') | O(smaller file) |
| top k | heapq.nlargest(k, c.items(), key=lambda kv: kv[1]) | O(u log k) time |
The answer to "how big does the dictionary get" is the number of distinct keys, not the number of lines. If keys are unbounded (every line has a unique request ID) say so and propose a fix (bucket by minute, external sort, approximate counting).
Worked example: the two-file join (the "dinosaur" pattern)
File 1: NAME,LEG_LENGTH,DIET. File 2: NAME,STRIDE_LENGTH,STANCE. Print the bipedal dinosaurs from fastest to slowest, where speed = ((STRIDE_LENGTH / LEG_LENGTH) − 1) × sqrt(LEG_LENGTH × g), g = 9.8.
import csv, math
G = 9.8
def read_rows(path):
with open(path, newline='') as f:
for row in csv.DictReader(f):
yield row
legs = {} # name → leg length (file 1 is small, index it)
for row in read_rows('dino1.csv'):
try:
legs[row['NAME']] = float(row['LEG_LENGTH'])
except (KeyError, ValueError):
continue # policy: skip malformed rows
speeds = []
for row in read_rows('dino2.csv'):
name = row.get('NAME')
if row.get('STANCE') != 'bipedal' or name not in legs:
continue # missing in file 1: can't compute, skip
try:
stride = float(row['STRIDE_LENGTH'])
except (KeyError, ValueError):
continue
leg = legs[name]
if leg <= 0:
continue
speeds.append((name, (stride / leg - 1) * math.sqrt(leg * G)))
for name, _ in sorted(speeds, key=lambda t: -t[1]):
print(name)
Complexity. O(a + b) to read both files, O(m log m) to sort the m bipedal matches, O(a) space for the index. Missing column? row['LEG_LENGTH'] raises KeyError, caught by the policy, or check the header once with reader.fieldnames and fail fast. Name in file 2 but not file 1? name not in legs skips it; say you'd log it. File 1 huge too? Index whichever is smaller, or sort both by name externally and merge like lab 9.
When the file is bigger than RAM
- Line-oriented aggregation: already fine, you only hold the state dict. Point this out.
- Random access to records (the fortune-cookie file): make one pass recording
f.tell()at the start of each record into a list of offsets (8 bytes per record, not the record itself), thenf.seek(offsets[random.randrange(len(offsets))])and read until the delimiter. Two passes, O(records) memory for offsets only. Read in'rb'so offsets are exact. - Sorting a huge file: external sort, chunks that fit in memory sorted and written, then a k-way merge with
heapq.merge. - Grouping with unbounded keys: partition by hash of the key into several files, then process each.
- Output could be huge: write it as you go instead of building a list.
- Mention generators:
def lines(path): with open(path) as f: yield from flets you chain filters lazily.
Reading a fortune file: records separated by a delimiter line
def fortunes(path):
buf = []
with open(path, encoding='utf-8') as f:
for line in f:
if line.rstrip('\n') == '%':
if buf:
yield ''.join(buf)
buf = []
else:
buf.append(line)
if buf:
yield ''.join(buf) # the last record may lack a trailing delimiter
import random
print(random.choice(list(fortunes('fortunes.txt')))) # small file
# big file: reservoir sampling, one pass, O(1) memory:
pick = None
for i, fortune in enumerate(fortunes('fortunes.txt'), 1):
if random.randrange(i) == 0:
pick = fortune
Interview questions
1. Why iterate the file instead of readlines()?
Iteration yields one line at a time, so memory is bounded by the longest line, not the file. readlines() materialises every line in a list: O(file) memory and a wasted pass.2. What does with do for you?
It guarantees f.close() runs when the block exits, including on exceptions, so you never leak file descriptors in a long-running script.3. When would you use the csv module instead of split(',')?
Whenever fields may be quoted or contain the delimiter or newlines.csv.reader follows the quoting rules; split does not. For plain colon-separated logs with no quoting, split is fine and faster.4. The second file has a key the first file doesn't. What happens in your code?
I look it up within or .get and skip or default it, and I'd count how many I skipped so the report is honest. I would not let a KeyError crash the run.5. A column is missing from the header. How does your code behave?
With DictReader,row['COL'] raises KeyError on the first row. I'd check reader.fieldnames against the required set before the loop and fail with a clear message, because every row would be wrong.6. What is the time and space complexity of the word-frequency program?
O(n) over the total words to count, O(u) space for u distinct words, plus O(u log u) if I sort the whole report or O(u log k) for a top-k with a heap.7. How do you handle a line with the wrong number of fields?
Checklen(parts), record the line number and reason in a list (or just a count), continue. State the policy before coding so the interviewer can redirect it.8. How would you return a random record from a file that doesn't fit in memory?
Either two passes with byte offsets and seek, or one pass with reservoir sampling: keep the i-th record with probability 1/i. Reservoir needs O(1) memory and one read; offsets need O(records) memory but give repeated O(1) picks.9. How do you sort by a computed value, descending, with a stable tie-break?
sorted(rows, key=lambda r: (-speed(r), r['NAME'])). Negate the number for descending and add the name so ties are deterministic; Python's sort is stable so equal keys keep input order.10. Your script works on 100 MB. What changes for 20 GB?
Nothing in the streaming loop. What could blow up is the state: distinct keys and any list of kept rows. Measure the key cardinality; if it is unbounded, partition by key hash to files or aggregate per time bucket. Also write output incrementally and consider reading with a larger buffer.11. What's the difference between json.load and json.loads?
load reads from a file object, loads parses a string. For JSON Lines you call loads per line; for a single big JSON array you'd need load (whole document in memory) or a streaming parser like ijson.12. Why open in binary mode for seek/tell?
In text mode, tell() values are opaque cookies and multi-byte UTF-8 characters mean character counts aren't byte counts. Binary mode gives exact byte offsets you can seek back to.Traps
- Forgetting the trailing newline:
'DOWN\n' != 'DOWN'. Strip first. split(',')on a quoted CSV field:"edge,west",100becomes three fields.- Counting lines instead of distinct keys when asked about memory.
- Building a list of every row "just in case" and then claiming O(1) memory.
- A bare
except:, or catching Exception and silently continuing without counting. - Comparing strings from the file with ints from the question without converting.
- Normalising inconsistently: counting
WEB-01andweb-01separately when they are the same host. - Using
f.read().split('\n')on a file you were told is large.
Do before marking this topic done
- Labs 1-6 below, timed, in a blank editor. Compare with the expected JSON only after your run.
- Word frequency: given a text file, print the 10 most common words (case-insensitive, punctuation stripped) with counts. Then answer out loud: complexity, dictionary size, what if the file is 50 GB.
- Reimplement the dinosaur join from the description above without looking. Add the missing-column check.
- Fortune file: implement both the offsets approach and reservoir sampling.
Practice: linked problems and labs
Logged attempts feed the tracker. Open the problem in a new tab, solve in a blank editor, then log honestly.
- LC 819Most Common WordEasy
- LC 1108Defanging an IP AddressEasy
- LC 1941Check if All Characters Have Equal Number of OccurrencesEasy
- LC 165Compare Version NumbersMedium
- LC 468Validate IP AddressMedium
- LC 811Subdomain Visit CountMedium
- LC 937Reorder Data in Log FilesMedium
- LC 71Simplify PathMedium
- LC 388Longest Absolute File PathMedium
- LC 751IP to CIDRMedium
- lab 1Count host failures from a fileLab
- lab 2Aggregate quoted CSV fieldsLab
- lab 3Keep the latest health recordLab
- lab 4Find the busiest clientsLab
- lab 5Compare two configuration snapshotsLab
- lab 6Track consecutive failures per hostLab
← 4 · IPv4, CIDR and subnetting · all topics · 6 · ARP, ICMP, DHCP, DNS and the life of a packet →