10 minutes read

The process of building web applications has been greatly simplified in today's world. One of the factors that has made this possible is the ability to reuse code. Can you imagine having to write your own function that generates a random number? While it is possible to write it, given that such functionality is already built out, you would rather spend more time on the unknown aspects of your application. Packages are an effective method that has contributed greatly to enabling code reusability.

In this topic, we will focus on learning and understanding what packages are and how they can be created and used.

What is a package and how to get one?

Each package is a collection of typically related modules and sub-packages stored under the same directory that contains an __init__.py file. The purpose of the __init__.py file is to mark the directory as a package. Packages are extremely handy as they enhance code organization in a hierarchical manner, promote code reuse, distribution, and encapsulation.

Below is a toy example of a laptop package: it contains 2 modules (dell.py and hp.py) and the Apple subpackage with its 2 modules (macbookair.py and macbookpro.py):

a file structure of a package

The standard package manager for Python is called pip. It is used for installing, upgrading, and generally managing Python packages from the Python Package Index (PyPI). PyPI is the software repository for the Python programming language where all Python packages are shared, you can think of it as of a central storage place.

With current Python versions, pip is usually included by default. You can use the following command to verify whether pip is installed in your system:

pip --version # or -v

You can use the following command to install:

python get-pip.py  # or python3 get-pip.py, depending on your Python version

To install a package using pip use the following command:

pip install package_name

For example, to install the numpy package you would use:

pip install numpy

Using an installed package: import

In order to incorporate an installed package into your program, you need to import it. Assuming we have installed the numpy package, we can use the import keyword to include it like so:

import numpy

This avails all the modules and sub-modules in the numpy package, as well as all the functions and declarations.

As a means to improve readability import is commonly used with aliases to give programmers a chance to write more descriptive code suited to their needs, or to replace excessively long package names. Alias can be declared using the askeyword, as shown below:

import numpy as np
import long_package_name as short_package
import long_package_name.long_subpackage_name as short_subpackage

This way you can use all those aliases inside your code. It is important to note that, while using import the last attribute should be a module or a subpackage, consequently, functions and class names cannot be used directly with import.

After imports with aliases you can then use them in your code as such:

array = np.array([1, 2, 3]) # instead of numpy.array()

short_package.some_function1()
short_package.some_function2()
# instead of long_package_name.some_function1() etc.

short_subpackage.other_function()
# instead of long_package_name.long_subpackage_name.other_function()

Using an installed package: from … import

Packages can also be imported using from keyword combined with import: it allows to import specific modules or subpackages without clogging up the namespace with unnecessary declarations. If we already know which functions or classes we'll need for our program, we can explicitly import only those, without importing any other declarations from the same module or subpackage. Let's consider our example from the previous section and import only the specific functions and classes we'll definitely use:

from numpy import array
from long_package_name import some_function1
from long_package_name.long_subpackage_name import other_function

Now our program will look as such:

array = array([1, 2, 3])

some_function1()
# some_function2() 
# Won't compile since we haven't imported some_function2()
# Neither will long_package_name.some_function2()

other_function()

Note how we can only use explicitly imported declarations. It is also important to mention that now the last attribute should be a module, a subpackage or a function/class, as shown above.

If you want to import all entities from a package or a subpackage, you can do so with the following syntax and then use them as such:

from long_package_name import *

some_function1()  # instead of long_package_name.some_function1()
some_function2()  # instead of long_package_name.some_function2()

Such imports are called wildcard imports. It is really handy in terms of writing code, since you no longer need to specify the package name to which an entity belongs. However, it could make debugging quite difficult since pinpointing the specific function with the error is a hassle.

Relative imports

Relative imports allow you to import modules or packages that are located in the same directory and depend on the current location of the module or package to be imported. They make use of the dot notation: a single dot means the module or package being imported is in the same directory as the current directory, while two dots would mean it is in the parent directory of the current location.

From our laptop example, for us to import the dell.py module into the hp.py module we would use

from . import dell

since they are located in the same directory. At the same time, in order to import the dell.py file into the macbookpro.py file, we would use

from .. import dell

It is important to note that relative imports are mostly useful when developing larger projects and therefore they may not work when using them in standalone scripts.

PEP Time!

Please note that, according to PEP 8, using wildcard imports is considered bad practice, as they make it unclear which names are present in the namespace, confusing both readers and automated tools. Absolute imports are recommended, as they are usually more readable. They also give better error messages if something goes wrong.

import package.subpackage.amateurs
from package.subpackage import amateurs

Explicit relative imports are also acceptable, especially when dealing with complex package layouts where using absolute imports would be unnecessarily verbose:

from . import animate           # in amazing.py, for example
from .barriers import function  # in animate.py, for example

Standard library code should avoid complex package layouts and always use absolute imports.

Multiple packages and Virtual environment

When you check the Python Package Index (PyPI), you will notice that most packages have different versions. This is due to constant upgrades made to the packages to improve their functionality. It is therefore a good convention to make use of the virtual environment when using multiple packages. A virtual environment gives you the ability to separate packages for your different Python projects, as well as isolate them from the global Python environment. Let's say you have two different projects that use the numpy package. Project 1 uses numpy 1.24, while Project 2 uses numpy 1.19. A virtual environment makes it possible to have these two different versions of the same package coexist without conflicts. It acts like a barrier that isolates one version from the other.

Conclusion

In this topic, we learned what a package is and what is its purpose. We went ahead and saw how to create a package and different ways of importing packages. We also explored why it is important to use the virtual environment when using multiple packages. Packages are an essential part of programming and we must understand how they work, and how we can use them to make our programs more efficient.

814 learners liked this piece of theory. 104 didn't like it. What about you?
Report a typo