How to Make Pytest Behave with Non-unique Filenames

I’ve been working on the NeetCode Blind75 problems, and I decided from the beginning that I wanted to do everything locally. I’ll submit the problem on NeetCode, but I want to have my own tests on my own local machine. (You can view my work at the GitHub repo).
I am doing the problems in Python and I’m using pytest
for testing. I started
out with a great directory structure with each problem having its own directory.
I originally had it set up so that each problem used its own unique filename and
test filename, e.g.:
neetcode-blind75/
|-- src/
|-- 01_arrays_hashing/ # problem category
|-- 01_contains_duplicate/ # individual problem
|-- contains_duplicate.py
|-- contains_duplicate_test.py
|-- README.md
Trying to keep things DRY
This was fine, and pytest
liked this structure, because of the default import
mode (more on that below), but it was a bit laborious: I had to make changes to
the test file every time I made a new problem. It used to be:
# 01_arrays_hashing/01_contains_duplicate/contains_duplicate_test.py
from contains_duplicate import contains_duplicate_brute
# ...
But I didn’t want to have to change that import line every time. What I really wanted was to keep it standardized:
from .solution import Solution
I have multiple solutions to each problem, so I first put all the solutions
within a Solution
class:
# contains_duplicate.py
class Solution:
def contains_duplicate_brute(nums: List[int]) -> bool:
# ...
def contains_duplicate_hashmap(nums: List[int]) -> bool:
# ...
Great. This cuts down on my work to initialize a new problem, but it also has the added benefit of mirroring how things work on Leet/NeetCode.
Then I made sure that every problem had consistent filenaming. I also knew that
I would need to make the folders a module/package to use relative imports (from .solution
vs from solution
), so I added a __init__.py
to each problem:
neetcode-blind75/
|-- src/
|-- 01_arrays_hashing/
|-- 01_contains_duplicate/
|-- __init__.py
|-- solution.py
|-- test.py
|-- README.md
I updated pyproject.toml
to account for the new test naming, and then…
(.venv) ➜ neetcode-blind75 git:(main) ✗ pytest
=========================================================================== ERRORS ===========================================================================
__________________________________ ERROR collecting neetcode_blind75_python/01_arrays_hashing/01_contains_duplicate/test.py __________________________________
ImportError while importing test module '/home/bradley/Projects/neetcode-blind75/neetcode_blind75_python/01_arrays_hashing/01_contains_duplicate/test.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/usr/lib/python3.11/importlib/__init__.py:126: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
neetcode_blind75_python/01_arrays_hashing/01_contains_duplicate/test.py:2: in <module>
from .solution import Solution
E ImportError: attempted relative import with no known parent package
================================================================== short test summary info ===================================================================
ERROR neetcode_blind75_python/01_arrays_hashing/01_contains_duplicate/test.py
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1 error in 0.12s
Yikes.
It’s quite a simple fix actually
ChatGPT was not very helpful in diagnosing this problem. It told me to change
PYTHONPATH
and to run the tests with python3 -m pytest src
. None of that
worked.
It was only when I Googled the actual error I was getting
(ImportError: attempted relative import with no known parent package
)
and read the Pytest documentation that I discovered the import mechanisms of Pytest.
By default, Pytest uses the prepend
import mode, which modifies sys.path
to
allow importing test modules directly as scripts. What you really want is to
configure it to use the importlib
import mechanism, which then utilizes the
actual, normal Python import system.
So all you have to do is run Pytest with the flag --import-mode=importlib
, and
away you go. You can also just add the options in your pyproject.toml
:
[tool.pytest.ini_options]
addopts = "-ra -q --import-mode=importlib" # <- here
minversion = "7.0"
pythonpath = ["."]
testpaths = ["src"]
python_files = ["test.py"]
Checklist
So, here’s all you need to have non-unique package names and test filenames in Pytest:
-
__init__.py
in every test folder - relative imports (e.g.
from .solution
notfrom solution
) -
--import-mode=importlib
in yourpyproject.toml
or on thepytest
command
Done. (And the tests pass.)