Why context managers replace manual open and close
The with statement opens a file, hands you the file object, then closes it on block exit. Closing matters: an unclosed file may keep buffered bytes in memory and never write them to disk, and the OS caps how many descriptors one process holds (1,024 on most Linux defaults). A loop that opens 5,000 log files without closing crashes with OSError: Too many open files.
The manual pattern f = open('data.txt'); ... ; f.close() looks fine until a line in between raises. The close() call never runs, the buffer never flushes, and Gradescope reports a missing output file. Wrap every open() in with and the close() is automatic, paired with the matching open even through exceptions.
A single with statement also chains multiple files in one line: with open('in.csv') as src, open('out.csv', 'w') as dst:. Both files close at block exit regardless of which one raised.
# Read a homework brief, count its lines, write the count to a result file
with open('brief.txt', 'r', encoding='utf-8') as src: # opens for reading, UTF-8 decoded
lines = src.readlines() # list of strings, one per line
line_count = len(lines) # integer count
with open('result.txt', 'w', encoding='utf-8') as dst: # opens for writing, truncates first
dst.write(f'Lines: {line_count}\n') # write expects a string, not bytes
print(f'Wrote {line_count} to result.txt') # observable output for grading
# Both files are already closed here, no f.close() needed