Packages
This session will explain how to build your own basic Python package, and why you might want to do so in the first place.
In this session, we will cover:
- When you might want to consider building a package
- How to make a Python module
- How to turn this module into a package
- How to share packages you’ve made with others
Why bother?
If you start regularly working with Python, you may find yourself frequently reusing the same bits of code. These might be things that are specific enough to your work that there isn’t already a built-in function or package available for it, but common enough that it’s annoying to keep copy/pasting the same functions.
Furthermore, if you make an improvement or fix a bug for some common code, you’d have to make this change everywhere you use it.
By splitting out reusable code into a package, you can reuse it in all your different projects, make improvements to it that benefit all the different places you use it, and share it with your colleagues in a more organised way (rather than emailing/Teams messaging code snippets around).
Definitions
People sometimes talk about python modules - these are essentially just .py files you address within your own project. A package is effectively a folder of modules that are related in some way and are treated as a unit.
If you’ve ever typed import pandas, you’ve used a Python package.
Creating a module with some functions
If you need a reminder on functions and how to define them, you might want to refer back to our session on functions.
Let’s imagine that there are two things we do regularly; check whether an NHS number is valid, and get an organisation name from an ODS code. We’ve written two functions (and have given them proper, Google-style docstrings for good measure):
def is_valid_nhs_number(nhs_number):
"""
Validate an NHS number using the Modulus 11 algorithm.
See: https://www.datadictionary.nhs.uk/attributes/nhs_number.html
Args:
nhs_number: NHS number to validate (string or int, spaces are ignored)
Returns:
bool: True if valid, False otherwise
"""
cleaned = str(nhs_number).replace(" ", "")
if not cleaned.isdigit() or len(cleaned) != 10:
return False
# step 1
digits = [int(d) for d in cleaned]
weights = [10, 9, 8, 7, 6, 5, 4, 3, 2]
products = [digit * weight for digit, weight in zip(digits[:9], weights)]
# step 2
total = sum(products)
# step 3
remainder = total % 11
# step 4
check_digit = 11 - remainder
# step 5
if check_digit == 11:
check_digit = 0
elif check_digit == 10:
return False
return check_digit == digits[9]And:
import requests
ODS_API_BASE = "https://directory.spineservices.nhs.uk/ORD/2-0-0/organisations"
def get_organisation_name(ods_code):
"""
Look up the name of an NHS organisation by its ODS code,
using the NHS Organisation Data Service (ODS) API.
Args:
ods_code (str): ODS code of the organisation, e.g. 'RHM' or 'RN1'
Returns:
str: The organisation name if found, or None if not found / on error
Example:
>>> get_organisation_name("RHM")
'UNIVERSITY HOSPITAL SOUTHAMPTON NHS FOUNDATION TRUST'
"""
ods_code = str(ods_code).strip().upper()
url = f"{ODS_API_BASE}/{ods_code}"
try:
response = requests.get(url)
if response.status_code == 404:
print(f"No organisation found for ODS code: {ods_code}")
return None
response.raise_for_status() # Catch any other HTTP error (500, etc.)
data = response.json()
return data["Organisation"]["Name"]
except requests.RequestException as e:
print(f"Error contacting ODS API: {e}")
return NoneLet’s name our proto-package of two functions nhsutils. We’ll define them in a file called nhsutils.py which just contains the two function definitions (as well as import requests which our ODS code lookup function relies on).
We can then, in a separate file in the same folder :
import nhsutilsor :
from nhsutils import is_valid_nhs_numberWe can then simply call our functions as normal:
print(is_valid_nhs_number(9960739791)) # TrueWe have successfully created a module.
A brief note about __name__ == "__main__"
If we wanted to, we could include some code in our package file that only runs when we run the file directly and doesn’t run if we’re just importing functions from the file as a module.
if __name__ == "__main__":
# print some example usage
print(is_valid_nhs_number(9012345678)) #returns False
print(is_valid_nhs_number(9960739791)) #returns True
print(get_organisation_name("RHU")) #returns "PORTSMOUTH HOSPITALS UNIVERSITY NHS TRUST"Turning what we’ve built into a package
Working with a package is very similar to just working with modules directly, just a little bit more structured and organised. We need to do two things:
- Create a folder, called
nhsutils, that contains our.pyfiles with our functions (either all in one big file, or preferably, split into different files). In our case, we’ll createnhsnumbers.pywith our NHS number validation function (and any other related functions we may want to write) andorganisations.pywith our ODS lookup function(s). - Create a file called (exactly)
__init__.py1 which allows us to, effectively, control which parts of our package we want users to interact with.
Our directory structure looks like this:
python-packages-example/
│ main.py
│
└───nhsutils/
│ __init__.py
│ nhsnumbers.py
│ organisations.pythe __init__.py file
from .organisations import get_organisation_name
from .nhsnumbers import is_valid_nhs_numberNote the slightly different syntax here; we’re using .name to indicate we want to import from files in the current folder (relative to __init__.py).
If our package had some intermediate functions that we only use internally, we can keep things tidy by not importing them in __init__.py. 2
Because we have an __init__.py which defines the final user-facing functions, they only need to call from nhsutils import is_valid_nhs_number; otherwise they would need to know the internal structure of our package and write from nhsutils.nhsnumbers import is_valid_nhs_number.
Conclusion
In this session we’ve gone from a couple of standalone functions to a properly structured, shareable Python package, without needing to do anything particularly complicated. The same pattern can scale to bigger projects: as you write more reusable code in your day-to-day work, dropping it into a package like this means you spend less time hunting for that function you wrote six months ago and more time actually using it. The NHS-specific examples here are just a starting point; take some time to think about code you reuse regularly or find yourself “reinventing” over and over again. A shared internal package is a straightforward way to address that, with the added benefit of sharing code and getting more eyes on it.
Further reading
Footnotes
names starting with double underscores are a convention in python used for internal identifiers (you might have come across these in previous sessions - e.g. object-oriented programming). They are usually pronounced “dunder” (from “double under[score]”), so
__init__becomes “dunder init”.↩︎https://www.geeksforgeeks.org/python/relative-import-in-python/↩︎