Python: create temporary files and directories in unittest

Sometimes, tests need temporary files or directories. You can do this in Python’s unittest with the standard library tempfile module. Let’s look at some recipes to do so within individual tests and setUp().
Creating a temporary file
To create a temporary file in a single test:
from tempfile import NamedTemporaryFile
from unittest import TestCase
class ExampleTests(TestCase):
def test_example(self):
temp_file = self.enterContext(
NamedTemporaryFile(mode="w+", suffix=".html"),
)
# Write to the file
temp_file.write("<h1>Cosmic Crisp</h1>")
# Read from the file
temp_file.seek(0)
result = temp_file.read()
self.assertEqual(result, "<h1>Cosmic Crisp</h1>")
Note:
enterContext()enters theNamedTemporaryFilecontext manager and runs its exit method at the end of the test. UsingenterContext()avoids indenting the whole test, as would be required usingwith.This method is new in Python 3.11. On older versions, you can copy a backport from my previous post.
The test uses
NamedTemporaryFileto create a temporary file. By using it as a context manager, its exit method deletes the temporary file. That prevents littering the filesystem with test files.NamedTemporaryFileis used instead ofTemporaryFile, to make the file visible in the filesystem. This allows other functions or processes to reopen it by name, available in thenameattribute. This is typically required when testing with files, where the system under test typically accepts a file name, like:render_apple(temp_file.name)
The
modeis set tow+, which means “reading and writing, text mode”. The default isw+b, which is the same but in binary mode. Pick text or binary to match what you’re testing.The
suffixis set to.html, which ensures the created file has that suffix. This isn’t mandatory, but it can make debugging easier. For example, if you need to open the file, it will open with its default program.
Creating a temporary directory
This works similarly:
from tempfile import TemporaryDirectory
from unittest import TestCase
class ExampleTests(TestCase):
def test_example(self):
temp_dir = self.enterContext(TemporaryDirectory())
# Write to a file
with open(f"{temp_dir}/apple.html", "w") as temp_file:
temp_file.write("<h1>Cosmic Crisp</h1>")
# Read from the file
with open(f"{temp_dir}/apple.html", "r") as temp_file:
result = temp_file.read()
self.assertEqual(result, "<h1>Cosmic Crisp</h1>")
Note:
NamedTemporaryFilehas been swapped forTemporaryDirectory. It creates a directory and completely removes it in its exit method.temp_diris not an object but a string containing the path of the temporary directory.
Per-test usage in setUp()
To have a temporary file available for each test, hoist the self.enterContext() to setUp():
from tempfile import NamedTemporaryFile
from unittest import TestCase
class ExampleTests(TestCase):
def setUp(self):
super().setUp()
self.temp_file = self.enterContext(
NamedTemporaryFile(mode="w+", suffix=".html")
)
def test_example(self):
# Write to the file
self.temp_file.write("<h1>Cosmic Crisp</h1>")
self.temp_file.seek(0)
# Read from the file
result = self.temp_file.read()
self.assertEqual(result, "<h1>Cosmic Crisp</h1>")
Or, for a temporary directory:
from tempfile import TemporaryDirectory
from unittest import TestCase
class ExampleTests(TestCase):
def setUp(self):
super().setUp()
self.temp_dir = self.enterContext(TemporaryDirectory())
def test_example(self):
# Write to a file
with open(f"{self.temp_dir}/apple.html", "w") as temp_file:
temp_file.write("<h1>Cosmic Crisp</h1>")
# Read from the file
with open(f"{self.temp_dir}/apple.html", "r") as temp_file:
result = temp_file.read()
self.assertEqual(result, "<h1>Cosmic Crisp</h1>")
Sprinkle in some pathlib
Using pathlib.Path simplifies the creation of test files within a temporary directory:
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest import TestCase
class ExampleTests(TestCase):
def setUp(self):
super().setUp()
self.temp_path = Path(self.enterContext(TemporaryDirectory()))
def test_example(self):
# Write to a file
(self.temp_path / "apple.html").write_text("<h1>Cosmic Crisp</h1>")
# Read from the file
result = (self.temp_path / "apple.html").read_text()
self.assertEqual(result, "<h1>Cosmic Crisp</h1>")
A bonus tip: pair with textwrap.dedent() when creating multi-line files. This can keep the text neatly indented within your test method but dedented in the temporary file:
from pathlib import Path
from tempfile import TemporaryDirectory
from textwrap import dedent
from unittest import TestCase
class ExampleTests(TestCase):
def setUp(self):
super().setUp()
self.temp_path = Path(self.enterContext(TemporaryDirectory()))
def test_example(self):
# Write to a file
(self.temp_path / "apple.html").write_text(
dedent(
"""
<html>
<body>
<h1>Cosmic Crisp</h1>
</body>
</html>
"""
)
)
# Read from the file
result = (self.temp_path / "apple.html").read_text()
self.assertIn("<h1>Cosmic Crisp</h1>", result)
Debug with delete=False
If tests are failing, you may wish to inspect the contents of a temporary file or directory. To disable the cleanup, add delete=False to the context manager, plus a print() to display the path. For example, for a file:
def setUp(self):
super().setUp()
self.temp_file = self.enterContext(
- NamedTemporaryFile(mode="w+", suffix=".html")
+ NamedTemporaryFile(mode="w+", suffix=".html", delete=False)
)
+ print("😅", self.temp_file.name)
Then when tests fail, the emoji will highlight the path (one of my print debugging tips) and you can inspect the file:
$ python -m unittest example
😅 /var/folders/20/lzgtdyzs7wj5fc_90w1h3/T/tmpgpynu7f_.html
F
======================================================================
FAIL: test_example (example.ExampleTests.test_example)
----------------------------------------------------------------------
...
FAILED (failures=1)
$ cat /var/folders/20/lzgtdyzs7wj5fc_90w1h3/T/tmpgpynu7f_.html
<h1>Cosmik Crisp</h1>
delete=False works similarly for TemporaryDirectory.
😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸
One summary email a week, no spam, I pinky promise.
Related posts: