← all topics

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.

Template to type first, then fill in:
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

Python Tutorial: File Objects - Reading and Writing to FilesCorey Schafer · 24:33

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.

Log File Parsing In Python (Part 1)Roberts Greibers · 14:04

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.

Python Tutorial: CSV Module - How to Read, Parse, and Write CSV FilesCorey Schafer · 16:11

csv.reader vs DictReader (4:16 header handling), delimiters, 12:23 DictWriter. Why split(',') breaks on quoted fields.

Python GeneratorsmCoding · 15:32

3:49 file processing with generators, 8:05 generator pipelines: the "file bigger than RAM" answer in code.

Collections in Python - Advanced Python 06Patrick Loeber · 14:09

0:30 Counter, 8:17 defaultdict, 10:34 deque. The three containers every parsing script uses.

Python Tutorial: Working with JSON Data using the json ModuleCorey Schafer · 20:33

0:40-10:59 is the standard-library part (loads/dumps, load/dump, indent). Watch before lab 3 and topic 15.

Python Tutorial: re Module - How to Write and Match Regular ExpressionsCorey Schafer · 53:17

Lookup video, not a sit-through: groups, named groups, finditer, compile.

The file object

CallReturnsMemoryUse when
for line in fone line at a time, newline keptO(longest line)default for any line-oriented file
f.read()whole file as one strO(file)small config files only
f.readlines()list of all linesO(file)almost never; say why not
f.read(n)next n characters/bytesO(n)records not separated by newlines (the fortune file), binary data
f.tell() / f.seek(pos)current byte offset / jump to oneO(1)index a huge file once, then random access
open(path, 'w'|'a')writerstream 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

AskStateCodeSpace
count per keyCounterc[host] += 1O(u) distinct keys
sum per keydefaultdict(int)total[iface] += nO(u)
group rows per keydefaultdict(list)by_region[r].append(row)O(n): you kept everything
latest record per keydictif ts > latest.get(h, (-1,))[0]: latest[h] = (ts, status)O(u)
distinct values per keydefaultdict(set)ips_by_user[u].add(ip)O(distinct pairs)
per-key streak / previous statedict of small tuplesrun[h] = run.get(h, 0) + 1 if status == 'DOWN' else 0O(u)
join two filesindex the smaller file in a dict, stream the largerregion = inv.get(host, 'UNKNOWN')O(smaller file)
top kheapq.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

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 with in 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?Check len(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

Do before marking this topic done

  1. Labs 1-6 below, timed, in a blank editor. Compare with the expected JSON only after your run.
  2. 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.
  3. Reimplement the dinosaur join from the description above without looking. Add the missing-column check.
  4. 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.

← 4 · IPv4, CIDR and subnetting · all topics · 6 · ARP, ICMP, DHCP, DNS and the life of a packet →