Structuring Python Projects
Contents
Structuring Python Projects#
This lesson will discuss how to organize the code in your Python project and how Python then finds your code.
Table of Contents
Modules#
A module is a file that contains Python code like that can be imported into other Python files. This can be used to split your code into multiple files so that your code is more organized.
"""Module of output formatting functions"""
WIDTH = 50
def header(title, width=WIDTH, char="-"):
underline = len(title) * char
lines = []
lines.append(title.center(width))
lines.append(underline.center(width))
return "\n".join(lines) + "\n\n"
def div(width=WIDTH):
return (width * "~") + "\n"
def section(title, contents):
text = ""
text += header(title)
text += contents + "\n\n"
text += div() + "\n"
return text
This can then be imported into another file.
import formatting
def main():
name = input("What's your name? ")
title = "Welcome."
contents = f"Well hello there {name.title()}."
text = formatting.section(title, contents)
print(f"\n\n{text}")
main()
Note
The term “module” is often used to refer to external code installed from the pipy.org repository. For example, someone might refer to the “requests module”. This is the broad software engineering usage of the word which refers to an interchangeable component like a plugin or library.
In the Python lexicon, these are called packages.
Packages#
A package is a group of modules in the same directory which also contains a
file named __init__.py
.
For example, imagine we have a hangman package with two modules. The
words.py
module contains a WORDS
list, and the game.py
module contains the functions to play the game.
hangman/
|-- __init__.py
|-- game.py
|-- words.py
You could import the whole package like so.
import hangman
hangman.play(hangman.words.WORDS)
Or import each module from the package separately.
from hangman import game
from hangman import words
game.play(words.WORDS)
Or import individual variables and functions from the modules.
from hangman.game import play
from hangman.words import WORDS
play(WORDS)
Subpackages#
You can further divide your code into subpackages, for example:
hangman/
|-- words/
|-- __init__.py
|-- easy.py
|-- medium.py
|-- hard.py
|-- __init__.py
|-- game.py
Variables and functions that are defined in your __init__.py
will be
available as part of your package.
from . import abort
Relative imports#
When importing from within the same package, you can use the shorthand .
.
from .words.easy import WORDS
from .game import play
def main():
play(WORDS)
If you’re importing from a subpackage you can reference the parent package by
using ..
. For example, from one of the words
modules we could import
the abort
function:
from .. import abort
The __init__.py
file#
The __init__.py
file is executed when the package is imported. This file
can be, and often is, blank. This can also be used to set up variables or
general purpose functions for your module.
__version__ = "0.0.1"
def abort(message):
print(f"ERROR: {message}")
exit(1)
The Python Search Path#
When Python looks for a module or package it checks:
the built-in modules and packages
the working directory
the list of paths stored in
sys.path
The sys.path
contains a list of path strings loaded from:
the environment variable
PYTHONPATH
any file ending in
.pth
located in the search path
Executable scripts#
When your code is in a package, it can be a little confusing how to go about running it in a reliable way. In this part of the lesson we’ll walk through how to create an executable script that imports your own package.
Here’s what our file structure will look like when we’re done.
hangman/
|-- __init__.py
|-- words.py
|-- game.py
|-- main.py
bin/
| -- play
Boil it down#
You first want to provide a minimal interface, a single function if possible,
that will be used by your script. So first we’ll create a main.py
file that
contains a single a main()
function.
from .words import WORDS
from .game import play
def main():
play(WORDS)
The script#
Traditionally the bin
directory (or Scripts
on Windows) is used to store
executable scripts. We’ll create the play
script there.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Executable to play the hangman game"""
from hangman.main import main
main()
This will only work when run from the project directory. However, we can
modify the sys.path
to include the project directory.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Executable to play the hangman game"""
import sys
from pathlib import Path
rootdir = Path(__file__).parent.parent
sys.path.append(str(rootdir))
from hangman.main import main
main()
Making the script executable#
Make it executable by running the chmod
command.
$ chmod +x bin/play
Running the script#
To run the script:
$ bin/play
Exercise#
(Hello World package)
Create a new directory called
helloworld
Create a subdirectory called
helloworld
Create an empty
__init__.py
file in that directory.Create a
hello.py
script in that directoryCreate a
main
function that prints"Hello World"
Create an executable
hello
script in a directory namedbin
orscripts
Add the project root directory to the
sys.path
.Import and call your your
main
function
Make the file executable
Run it
Bonus: Add the
helloworld/bin
directory to yourPATH
, then runhello
from a different directory.
(Goodbye)
Add a
goodbye
module that contains a functiongoodbye()
that prints"Goodbye cruel world!"
Import the
goodbye()
function in thehello
moduleIn the
main()
function, after printing"Hello World"
, callgoodbye()
.
(Refactor)
Refactor your code so that you end up with:
a
goodbye
module that contains agoodbye()
functiona
hello
module that contains ahello()
function that just prints"Hello World!"
a new
main
module thatimports the
hello()
andgoodbye()
functions from their respective modulescontains a
main()
function that callshello()
andgoodbye()
Modify your
hello
script to import from themain
module instead ofhello
Directory structure#
|-- .git/
|-- config
|-- ...
|-- .venv/
|-- bin/
|-- ipython
|-- pip
|-- pytest
|-- python
|-- ...
|-- ...
|-- .vscode/
|-- settings.json
|-- ...
|-- bin/
| -- play
|-- docs/
|-- hangman/
|-- __init__.py
|-- words.py
|-- game.py
|-- main.py
|-- tests/
|-- __init__.py
|-- test_words.py
|-- test_game.py
|-- test_main.py
|-- .editorconfig
|-- .env
|-- .gitignore
|-- README.md
|-- LICENSE
Directories#
.git#
This is the folder that is created when you use the git init
command and
contains all of your repository data. You will rarely deal with the files in
this directory directly, but the one file that is useful to know about is the
config
file.
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/alissa-huskey/python-class.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
.venv/#
Your virtual environment is stored in this directory if you set up poetry to store your virtual environment in your project directories, and it is the standard directory for some other virtual environment tools.
The executable scripts python
, pip
, and any installed modules are located
in the .venv/bin
directory.
This directory also contains a .venv/bin/activate
. This file which is
used to start your virtual environment shell with the command
source .venv/bin/activate
, upon which it defines a deactivate
command.
VS Code will automatically activate source the .venv/bin/activate
file in some cases, though not consistently enough to avoid the need to
launch VS Code from a poetry
shell.
VS Code may also prompt you upon finding a .venv
directory to select it for
the workspace. This will automatically configure some of the settings in your
per-project settings.json
file.
Poetry setup#
To set up poetry
to store your virtual env in your project directories.
$ poetry config virtualenvs.in-project true
You will need to move the existing virtual env directory.
$ mv $(poetry env info -p) ./.venv
.vscode/#
This is the directory where the vscode settings for this project are stored.
Here is an example that that sets the Python interpreter for this project to
your projects .venv
directory.
{
"python.venvPath": "${workspaceFolder}/.venv",
"python.pythonPath": "${workspaceFolder}/.venv/bin/python",
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.nosetestsEnabled": false,
"python.testing.pytestEnabled": true
}
bin/#
As previously mentioned, this is generally where executable scripts are stored.
docs/#
This is generally where any project documentation is stored.
hangman/#
This is the directory for the hangman
package. Package names should have
short lowercase named without underscores.
The module file should have short all lowercase names with underscores as needed.
tests/#
Tests are stored in here. Test files should be named
test_module_or_topic.py
.
Files#
.editorconfig#
Used in conjunction with the EditorConfig VS Code extension, this file configures code style settings for the project to ensure consistency amongst different tools, environments and developers. Here is a comprehensive example for Python files.
# EditorConfig: https://EditorConfig.org
# Plugins for:
# - Vim: https://github.com/editorconfig/editorconfig
# - VS Code: https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
[*.py]
indent_style = space
indent_size = 4
tab_width = 4
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 88
quote_type = double
spaces_around_operators = true
You can keep a master .editorconfig
file in your home directory, then
in your per-project file either inherit selective settings by removing the
root = true
line, or keep it to override all settings.
.env#
This is a shell script file that allows you to configure environment variables or run other commands for this project.
README.md * Make a README
.gitignore * github Python template * github/gitignore * github.io Python template * gitignore.io * man page * The Git Book > Ignoring Files
.vscode-exclude
See also#
Glossary#
Structure#
- module#
a file containing of reusable code, like function definitions and global variables, that can be imported into other programs
- package#
a directory containing a
__init__.py
file and other Python modules- namespace#
The group that a variable or function is part of.
print()
is part of the global namespace whereasrandint()
is part of therandom
module, and therefore part of therandom
namespace.