Tiny Python refactorings: try/except - with
Using try/except and with to better organize the flow of a program.
Scenario: Execute a command based on an id, if this id is present in the given file, otherwise execute a different command and save the resulting id in a file. This will be picked up later in a future execution.
A naive implementation
import subprocess
def do_stuff():
"""
Execute a command based on an id,
if a certain id is present in the given file.
"""
file = open("last_id.txt")
id = file.readline()
if id:
subprocess.run(["echo", id])
file.close()
return
file.close()
"""
Execute a command and save the resulting id in a file.
This will be picked up later in a future execution.
"""
completed_process = subprocess.run(
["echo", "1673"], stdout=subprocess.PIPE, universal_newlines=True
)
file = open("last_id.txt", "w")
file.write(f"{completed_process.stdout}\n")
file.close()
do_stuff()
The problem with this is code is that it throws because it can't find the file on the first execution:
FileNotFoundError: [Errno 2] No such file or directory: 'last_id.txt'
To solve this problem, let's wrap the first block of code in a try
block, and the second part in except
:
import subprocess
def do_stuff():
try:
file = open("last_id.txt")
id = file.readline()
if id:
subprocess.run(["echo", id])
file.close()
file.close()
except FileNotFoundError:
completed_process = subprocess.run(
["echo", "1673"], stdout=subprocess.PIPE, universal_newlines=True
)
file = open("last_id.txt", "w")
file.write(f"{completed_process.stdout}\n")
file.close()
do_stuff()
This ensures that:
- the program doesn't halt
- if the file doesn't exist yet, it's created on first run
You can read about try/except
here.
However, this tiny refactoring does not solve completely the problem, because if the first subprocess.run()
fails, we're still left with an open file descriptor hanging around.
This can be reproduced by running python with -X dev
(thanks to Sviatoslav for this trick), and by misspelling the command in the call to .run()
:
if id:
subprocess.run(["eco", id])
file.close()
return
file.close()
The result:
ResourceWarning: unclosed file <_io.TextIOWrapper name='last_id.txt' mode='r' encoding='UTF-8'>
file = open("last_id.txt", "w")
To make the code more robust, we can use a with
context manager, which is able to close files for us:
import subprocess
def do_stuff():
try:
with open("last_id.txt") as file:
id = file.readline()
if id:
subprocess.run(["echo", id])
return
except FileNotFoundError:
completed_process = subprocess.run(
["echo", "1673"], stdout=subprocess.PIPE, universal_newlines=True
)
with open("last_id.txt", "w") as file:
file.write(f"{completed_process.stdout}\n")
do_stuff()
Thanks to Harry and Sviatoslav for catching a couple of bugs in my example.