Creating A Module
Last updated on 2025-09-28 | Edit this page
Overview
Questions
- How do I create a Python module?
- How do I import a local module into my code?
Objectives
- Create a Python module with multiple files
- Import functions from a local module into a script
Project Organization
In order to keep our project organized, we’ll start by creating some directories to put our code in. So that we can keep the “source” code of our project separate from other aspects, we’ll start by creating a directory called “src”. In this directory, we’ll create a second directory with the name of our module, in this case “textanalysis-tool”. We can also delete the “main.py” file that was generated automatically by uv. Your project folder should now look like this:
textanalysis-tool/
├── src/
│ └── textanalysis_tool/
├── .gitignore
├── .python-version
├── pyproject.toml
└── README.md
Note that the interior folder has an underscore instead of a hyphen.
We will import the module using the name of the interior folder,
textanalysis_tool
. This is important as hyphens are not a
valid character in Python module names.
Next, we’ll create a file called __init__.py
in the
src/textanalysis_tool
directory. This file will make Python
treat the directory as a package. Next to the __init__.py
file, we can create other Python files that will contain the code for
our module.
The __init__.py
file is a special filename in python
that indicated that the directory should be treated as a package. Often
these files are simply blank, however we can also include some
additional code to initialize the package or set up any necessary
imports, as we will see later.
Let’s create a code file now, called say_hello.py
and
put a simple function in it:
Our project folder should now look like this:
textanalysis-tool/
├── src/
│ └── textanalysis_tool/
│ ├── __init__.py
│ └── say_hello.py
├── .gitignore
├── .python-version
├── pyproject.toml
└── README.md
Previewing Our Module
It’s all well and good to write some code in here, but how can we actually use it? Let’s create a python script to test our module.
Let’s create a directory called “tests”, and start a new file called
test_say_hello.py
in it.
Add the following code to it:
PYTHON
import textanalysis_tool
result = textanalysis_tool.say_hello.hello("My Name")
if result == "Hello, My Name!":
print("Test passed!")
else:
print("Test failed!")
One of the really nice things about using uv
is that we
can replace python
in our commands with uv run
and it will use the environment we have created for the project to run
the code. At the moment, this doesn’t make a difference, but once we
start adding dependencies to our project we’ll see how useful this
is.
Let’s run the script from our command line. If you’re in the root
directory of the project, your command will look something like
uv run tests/test_say_hello.py
.
Aaaand… It doesn’t work!
PYTHON
D:\Documents\Projects\textanalysis-tool>uv run tests/test_say_hello.py
Traceback (most recent call last):
File "D:\Documents\Projects\textanalysis-tool\tests\test_say_hello.py", line 1, in <module>
from textanalysis_tool.say_hello import hello
ModuleNotFoundError: No module named 'textanalysis_tool'
The reason for this is that we never actually told python where it can find our code!
The Python PATH
When you run a command like import pandas
, what python
actually does is search the a series of directories in order looking for
a module file called pandas.py
. We can see what directories
will be checked by printing the sys.path
variable.
We are only interested in checking our current code, not in
installing it as a package. However because we have the
__init__.py
file in our package directory, if we add the
exact or relative path to our package directory to our
sys.path
variable, python will look there for modules as
well.
Let’s add a quick line to the top of our testing script to add our specific module directory to the path:
PYTHON
import sys
sys.path.insert(0, "./src")
from textanalysis_tool.say_hello import hello
result = hello("My Name")
if result == "Hello, My Name!":
print("Test passed!")
else:
print("Test failed!")
This time it works! And if we modify the hello
function
to print out something slightly different, we can see that running the
script changes the output right away!
We used sys.path.insert
instead of
sys.path.append
because we want to give our module
directory the highest priority when searching for modules. This way, if
there are any naming conflicts with other installed packages, our local
module will be found first.
Ideally you would never be working with a package name that has a conflict elsewhere in the path directories, but just in case, this avoids some potential issues.
Dot Notation in Imports
You probably noticed that our function call mimics the file and
directory structure of the project. We have the project directory
(textanalysis_tool
), then the filename
(say_hello
), and finally the function name
(hello
). Python treats all of these similarly when trying
to locate a function or module. We can also modify our import to make
the code a little neater:
PYTHON
import sys
sys.path.insert(0, "./src")
from textanalysis_tool import say_hello
result = say_hello.hello("My Name")
if result == "Hello, My Name!":
print("Test passed!")
else:
print("Test failed!")
or even better:
PYTHON
import sys
sys.path.insert(0, "./src")
from textanalysis_tool.say_hello import hello
result = hello("My Name")
if result == "Hello, My Name!":
print("Test passed!")
else:
print("Test failed!")
Fos simplicity here, we are using the
from ... import ...
syntax to import only the function we
need from the module. This way we don’t have to include the module name
every time we call the function.
This is a common practice in python, however the absolute best practice would be to import the entire module and then call the function with the module name, as in the first example. This way we avoid potential naming conflicts with functions from other modules, as well as providing clarity about where the function is coming from when reading the code.
The init.py File
We can see that in order to import our function, we have to include
both the name of the module and the name of the file before specifying
the name of the function. Sometimes this can get tedious, especially if
there is a directory in our project with lots of files and different
functions in each file. This is where we can simplify things a little by
adding a tiny bit of code to our __init__.py
file:
We can run our testing script just the same way as before and it will
still work, but we can also now leave out the .say_hello
part:
Git Add / Commit
Before we forget, now that we have some simple code up and running, let’s add it to our git repository.
Challenge 1: Add a Sub Module
Create a directory under src/textanalysis_tool
called
greetings
and add a file called greet.py
with
a function called greet that prints out the following:
Hello {User}!
It's nice to meet you!
How do you call this function from your testing script?
You could also include this line in our existing
__init__.py
file:
or add another __init__.py
and include the following
line:
Challenge 2: Inter-Module imports
Building on the last challenge, the first line of our greeting is
identical to the output from our hello
function. How can we
avoid code duplication by calling the hello
function from
within our greet
function?
Why are you bothering to write out the entire module path in greetings.py? Can’t you just do this:
Yes, you absolutely can! The dot-notation used in python pathing can
use ..
to refer to the parent directory, or even
...
to refer to a grandparent directory.
The reason we’re not doing this here is for clarity, as recommended in PEP8.
- Python modules are simply directories with an
__init__.py
file in them - You can add the path to your module directory to
sys.path
to make it available for import - You can use dot notation in your imports to specify the module, file, and function you want to use