Python10 min readCode verified

Dictionaries, Files, and the Mode That Deletes Your Data

One of the three file modes empties the file the moment you open it, before you have written a single byte. Plus lookup by name instead of by number, and how to stop a crash from ending your program. Every result on this page was executed, not remembered.

Lookup by name instead of by number#

A list finds things by position. A dictionary finds them by a key you choose:

item = {"name": "Water", "amount": 10}
print(item["name"])
item["amount"] = 20
print(item)
Water
{'name': 'Water', 'amount': 20}

Same square brackets as a list, but what goes inside is a key rather than an index — item["name"], never item[0]. Assigning to a key that does not exist adds it; assigning to one that does replaces its value. There is no separate "add" operation.

Keys are usually strings, and they must be immutable — which rules out using a list as a key:

d = {}
d[["a"]] = 1
Traceback (most recent call last):
  File "d.py", line 2, in <module>
    d[["a"]] = 1
    ~^^^^^^^
TypeError: unhashable type: 'list'

"Unhashable" is Python's word for "this could change underneath me, so I cannot use it to file things by." A tuple works; a list does not.

KeyError, and the two ways to avoid it#

item = {"name": "Water"}
print(item["price"])
Traceback (most recent call last):
  File "k.py", line 2, in <module>
    print(item["price"])
          ~~~~^^^^^^^^^
KeyError: 'price'

The key is missing. In real code the cause is almost always a typo, a case mismatch"Name" is a different key from "name" — or one item in a collection built with a different shape from the rest.

item = {"name": "Water"}
print(item.get("price"))
print(item.get("price", 0))
print("name" in item)
None
0
True

Use the brackets when a missing key means your program is wrong and you want to hear about it. Use .get() when a key is legitimately optional. in asks about keys, never values.

Looping over a dictionary#

d = {"one": 1, "two": 2}
print(list(d.keys()))
print(list(d.values()))
for k, v in d.items():
    print(k, v)
['one', 'two']
[1, 2]
one 1
two 2

.items() hands you both at once, which saves a lookup inside the loop. Since Python 3.7 a dictionary keeps its insertion order — but if you need a particular order, ask for it:

roster = {"Jill": "A", "Jack": "B"}
for k in sorted(roster.keys()):
    print(k, roster[k])
Jack B
Jill A
d = {"a": 1, "b": 2}
del d["a"]
print(d, len(d))
{'b': 2} 1

The counting idiom#

This is the single most reused dictionary pattern, and it is worth memorising as one line:

words = ["a", "b", "a", "c", "a"]
counts = {}
for w in words:
    counts[w] = counts.get(w, 0) + 1
print(counts)
{'a': 3, 'b': 1, 'c': 1}

Read it right to left: get the current count or zero if this is the first time, add one, store it back. The 0 default is what makes the first sighting of an item work without a special case — write it with plain brackets and the first counts[w] + 1 raises KeyError.

A list of dictionaries#

Records with named fields, in order — the shape most small data sets take:

supplies = [{"name": "Water", "amount": 1}, {"name": "Radio", "amount": 2}]
for it in supplies:
    print(it["name"], it["amount"])
Water 1
Radio 2

The loop hands you one dictionary per pass; the keys read its fields. Every record needs the same keys — one item missing "amount" stops the loop with a KeyError partway through, after some output has already been printed.

Reading and writing files#

Use with. It closes the file for you, including when something raises partway through:

with open("demo.txt", "w") as f:
    f.write("first\nsecond\n")

with open("demo.txt") as f:
    print(f.read())
first
second

Note that .write() does not add a newline — if you want lines, you write the \n yourself.

ModeWhat it does
"r"read; the default, and an error if the file does not exist
"w"write; empties the file immediately, or creates it
"a"append; adds to the end, or creates it
The destructive one

"w" truncates the file the moment it is opened, before your first write. Open a file for writing to "check something" and its contents are already gone. There is no warning and no undo.

with open("demo.txt", "w") as f:
    f.write("first\n")

with open("demo.txt", "w") as f:
    f.write("second\n")

with open("demo.txt") as f:
    print(f.read())
second

Same code with "a" on the second open keeps both:

with open("demo.txt", "w") as f:
    f.write("first\n")

with open("demo.txt", "a") as f:
    f.write("second\n")

with open("demo.txt") as f:
    print(f.read())
first
second

Reading line by line, and the extra blank lines#

Looping over an open file gives you one line at a time — and each one arrives with its newline still attached:

with open("demo.txt", "w") as f:
    f.write("first\nsecond\n")

with open("demo.txt") as f:
    for line in f:
        print(repr(line))
'first\n'
'second\n'

repr() is the trick that shows it. Because the line already ends in a newline and print adds another, output comes out double-spaced. .strip() fixes it:

with open("demo.txt", "w") as f:
    f.write("first\nsecond\n")

with open("demo.txt") as f:
    for line in f:
        print(line.strip())
first
second

.readlines() gives the whole file as a list in one go — convenient for a small file, and the newlines are still there:

with open("demo.txt", "w") as f:
    f.write("first\nsecond\n")

with open("demo.txt") as f:
    print(f.readlines())
['first\n', 'second\n']

Combine that with .split() and you can read a data file into fields: loop the lines, strip each one, split it on the separator, convert what needs converting.

Exceptions#

A missing file stops the program:

f = open("nope.txt")
Traceback (most recent call last):
  File "o.py", line 1, in <module>
    f = open("nope.txt")
        ^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'nope.txt'

try/except lets you carry on instead:

try:
    f = open("nope.txt")
except FileNotFoundError:
    print("Could not open the file.")
Could not open the file.

Name the exception you expect. A bare except: swallows everything — including the typo in the line below it, which then produces no error and no output, and leaves you debugging a program that quietly does nothing.

Catching the exception object gives you the message:

try:
    n = int("hi")
except ValueError as e:
    print("ValueError:", e)
ValueError: invalid literal for int() with base 10: 'hi'

That is the exception behind every int(input()) on a user who typed a word, which makes it the one worth handling in a first course.

The full form has four parts:

def divide(x, y):
    try:
        result = x // y
    except ZeroDivisionError:
        print("cannot divide by zero")
    else:
        print("answer is", result)
    finally:
        print("done")

divide(10, 2)
divide(10, 0)
answer is 5
done
cannot divide by zero
done

else runs only when nothing was raised. finally runs either way — note done appears under both calls. Keep the try block down to the line that can actually fail and put the rest in else; a large try hides which line you were guarding against.

ExceptionTypically means
KeyErrora dictionary key that is not there — often a typo or wrong case
IndexErrora list or string position past the end
ValueErrorright type, impossible value — int("hi")
TypeErrorwrong type for the operation — "I am " + 20
NameErrora name that has not been defined — usually a misspelling
FileNotFoundErrorthe path is wrong, or you are not in the directory you think
ZeroDivisionErrora divisor that reached zero

Where these go wrong#

  • Indexing a dictionary by number. item[0] looks for a key 0, not the first entry.
  • Case mismatch in a key. "Name" and "name" are different keys. A very common KeyError.
  • counts[w] + 1 on the first sighting. KeyError. Use counts.get(w, 0) + 1.
  • Opening for writing to inspect a file. "w" empties it on open. Use "r", or "a".
  • Forgetting .strip() on file lines. Double-spaced output, and comparisons that fail because of an invisible \n.
  • Expecting .write() to add newlines. It does not. Write \n yourself.
  • A bare except:. Hides real bugs, including typos on the very next line.
  • A try block wrapping half the program. You lose track of which line you were guarding.
  • Assuming every record has the same keys. One odd item raises partway through, after output has already been printed.

More Python guides

Free study guides for a first Python course — every listing executed before publication.

See the Python course →

Frequently asked questions#

What is a KeyError in Python?

It means you asked a dictionary for a key it does not hold. Usually the cause is a typo or a case mismatch — "Name" is not "name" — or an item built with a different shape from the others. Use .get(key, default) or the in operator when a key may legitimately be missing.

What is the difference between d[key] and d.get(key)?

d[key] raises KeyError when the key is absent. d.get(key) returns None instead, and d.get(key, default) returns whatever default you supply. Use the brackets when a missing key is a bug, and .get() when it is expected.

How do I count things in Python?

Use a dictionary and the .get() idiom: counts[item] = counts.get(item, 0) + 1. It reads the current count or starts from zero, adds one, and stores it back — so it works on the first sighting of an item as well as the hundredth.

Why did opening a file for writing delete everything in it?

Because mode "w" truncates the file to empty the moment it is opened, before you write anything. If you want to add to a file rather than replace it, open it with "a" for append.

Why do my printed file lines have blank lines between them?

Because each line read from a file still carries its trailing newline character, and print adds another. Use line.strip() to remove it, or print(line, end='').

What is the difference between else and finally in try/except?

The else block runs only when no exception was raised. The finally block runs either way, exception or not, which makes it the place for cleanup that must happen regardless.