Working with multiple files

Day 18: Imports

Python guru sitting down with a screen instead of a face, and day 18 displayed on it.

Welcome to day 18 of the 30 Days of Python series! Today we're going to be talking about the very important topic of imports.

So far we've been working with the core Python syntax, and we've been using the so called built-in functions. However, there's a lot more to the Python standard library than just these functions, but to get access to it, we need to be able to import this functionality.

Importing will also let us use 3rd party libraries, which we'll cover later in the series.

Basic imports

We're going to start off by importing the math module, which, as the name implies, contains a lot of useful tools for performing mathematical operations. It also includes several useful constants. You can find documentation here.

To import a module we need to write the import keyword, followed by the name of the module we want to import. To import the math module, we'd write this at the top of our code:

import math

Just like that, we have access to all of the functionality defined in the math module. In order to use these functions and variables, we just need to put math. before the function or variable name. This tells Python that we're requesting something located in the math module.

Let's see this in action with an example.

All the way back in day one of the series, we had the following exercise question:

Calculate and print the area of a circle with a radius of 5 units.

This would be a perfect application for the math module, as we can make use of the math module's pi constant:

import math

print(math.pi * 5**2)  # 78.53981633974483

This is a great deal more accurate than my original solution, which used 3.141 as the value for pi.

Now let's try a function instead. The math module has a function called fsum which is for adding together floating point numbers. You can certainly use regular old sum for this, but things get a little bit weird for certain values, because there's an inherent inaccuracy in floating point numbers.

Take the following example:

numbers = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
print(sum(numbers))  # 0.9999999999999999

That's right. Adding ten lots of 0.1 is not 1. You'll get similarly dodgy results if you try adding 0.2 over and over again as well.

However, if we use the fsum function, mathematics goes right back to normal:

import math

numbers = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
print(math.fsum(numbers))  # 1.0

If you're interested to know why 0.1 produces this unusual result, I recommend you check out my post on the Decimal module. At the start I talk about how decimal numbers are represented in a computer, and why this leads to problems with floats.

Style note

Make sure to always put your imports at the top of the file! This is not only good style, there are also good practical reasons for this, which we'll get to shortly.

It's also a good idea to put a space after you're done with your imports to separate them from the rest of your code.

Importing specific things from modules

Let's say we're working on a project and I really only need the pi constant from the math module, and I plan to use it extensively in my code. There's no real problem with using import math and then referring to the constant using math.pi, but this is going to get a little tedious.

In this situation it would be a lot better if we could just use pi in our code, and we can do this by importing just the pi constant from the math module. The syntax for this is as follows:

from math import pi

We can import several things from a module by separating the names with commas. Let's say I also want the tau constant:

from math import pi, tau

Now we can refer to pi or tau directly instead of using math.pi and math.tau. However, we no longer have access to the rest of the math module using this approach, so we can't call math.fsum unless we also import math.

In some modules there are also things we can only access through this specific import syntax.

For example, let's say we want to work with tkinter, because we've decided we want to make an application with a graphical user interface (a GUI).

The tkinter library includes a set of themed widgets that we can use in our applications, but in order to use them, we have to import ttk from the tkinter module.

from tkinter import ttk

Modules and namespaces

Now that we've seen two different types of import, it's worth talking about what happens when we import a module, and why we access things in different ways depending on how we import them. For example, why do we need to put this math. in front of pi when importing it one way, but not the other?

Let's use the same technique as we did in day 13 and look at what gets defined in the global scope when we use the two different import methods.

First, let's try import math. We can see what is in the global namespace like so:

import math

print(globals())

What we get is a dictionary like this:

{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': None,
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f23b99febb0>,
    '__spec__': None,
    '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>,
    '__file__': 'main.py',
    '__cached__': None,
    'math': <module 'math' from '/usr/local/lib/python3.8/lib-dynload/math.cpython-38-x86_64-linux-gnu.so'>
}

Just like before we have a lot of stuff defined here when we start our program, but right at the end we have a name called math. Associated with this name math we have something called a module object, and in this case we can see that Python has found the code for this module somewhere in our Python installation.

When we import a module, Python runs the module and creates this module object so that we can access the things inside of it. In order to indicate we want to do something with the module, we have to first access the module using the name it's assigned to (in this case, math), and then we can access the things inside using the dot notation.

Now let's look at what happens when we import specific items from the math module. Once again I'm going to use the pi and tau constants, so now our code is going to look like

from math import pi, tau

print(globals())

And we get the following output:

{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': None,
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fda798e3bb0>,
    '__spec__': None,
    '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>,
    '__file__': 'main.py',
    '__cached__': None,
    'pi': 3.141592653589793,
    'tau': 6.283185307179586
}

One thing to notice is that we no longer have this math module object, and nothing is assigned to the name math in the global scope. This is why we're no longer able to access things like fsum using something like math.fsum.

What we do have are two new names, pi and tau, and these names are assigned long float values. This is what we're referring to when we use the names pi and tau. They're just normal variables that Python went ahead and defined for us when we imported the constants.

Important

As I mentioned in this section, when we import a module, Python has to run the module. This makes a lot of sense, because some values or functions may rely on other values in the module. No matter what style of import we use, Python always has to run the module.

One thing that a lot of Python developers mistakenly assume is that using an import like this:

from math import pi, tau

Is somehow more efficient, because we're only grabbing a couple of values, and Python therefore didn't have to run the module. This isn't the case, so please don't be tempted to use this specific import syntax because you want to make the module load more efficient.

There is, however, one important way that the syntax above can help with efficiency, and that's when referencing the values. If you're using a name many, many times in a performance sensitive part of your application, referring to pi directly will be marginally faster than math.pi. This is very rarely something you're going to have to worry about though, and the difference in speed is very minor. You need millions of references for there to be any noticeable difference.

Aliased imports

Sometimes module names are fairly long, or we just have to refer to them many times, and including the module name whenever we call a function defined in that module can become a little cumbersome. For this reason, we can use an alias for a module when we import it. This allows us to refer to the module using this alias, rather than the module's name.

For example, let's say we're doing some data analysis work and we've decided to use the numpy module.

It's common convention when importing numpy to import it using the name np, which we can accomplish using the as syntax.

import numpy as np

What we've done here is set an alias for the numpy module, which just means we've asked Python to let us refer to it by some other name. In this case, we've asked to refer to it using np.

Let's print what ends up in the global namespace in this case:

import numpy as np

print(globals())

If you're running this code on repl.it, you're going to get something like the output below. If you're running this in a local Python installation, the code may not work, because numpy is a third party module, and has to be installed.

{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': None,
    '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fa4ee853bb0>,
    '__spec__': None,
    '__annotations__': {},
    '__builtins__': <module 'builtins' (built-in)>,
    '__file__': 'main.py',
    '__cached__': None,
    'np': <module 'numpy' from '/home/runner/.local/share/virtualenvs/python3/lib/python3.8/site-packages/numpy/__init__.py'>
}

As we can see, we once again get a module object, and the module's name is 'numpy', but Python has assigned this module to the name np.

This means we can access things in the numpy module using np., such as the array type. We'd do this by typing np.array.

Importing using *

In addition to the import syntax we've looked at so far, we can also write something like this:

from math import *

This syntax essentially means, "Give me everything in the math module". We don't quite get everything, as we're still not going to get access to module contents that require a specific import, like when importing ttk from tkinter.

I want to make it very clear up front that this is not a style of import you should be using in your code 99% of the time. The reason for this is that all of the names defined in the module we're importing are going to get added to the global namespace.

If you want to see this for yourself, run the following code.

from math import *

print(globals())

This is a big problem, because it can very easily lead to name conflicts. For example, we might accidentally use a name of something defined in a module we've imported, and then we've overwritten the value we got from the module. If different modules also use the same names for values, we're also going to run into issues where one is overwriting the other.

For example, both math and numpy define a value called pi, but only one value can be assigned to the name pi in our global namespace.

If it's a bad idea to use this style of import, then why does it exist? It's mostly there to facilitate quick testing. If we're not working on a serious application, using the * import can save us a lot of typing where we don't need to worry about best practices.

Just don't use it in any serious applications you're writing unless you have a genuine use case and you know what you're doing.

Exercises

1) Import the fractions module and create a Fraction from the float 2.25. You can find information on how to create fractions in the documentation.

2) Import only the fsum function from the math module and use it to find the sum of the following series of floats:

numbers = [1.43, 1.1, 5.32, 87.032, 0.2, 23.4]

3) Import the random module using an alias, and find a random number between 1 and 100 using the randint function. You can find documentation for this function here.

4) Use the randint function from the exercise above to create a new version of the guessing game we made in day 8. This time the program should generate a random number, and you should tell the user whether their guess was too high, or too low, until they get the right number.

You can find our solutions to the exercises here.

Additional Resources

If you want to learn more about GUI development, we have a dedicated course on Udemy, as well as a number of posts on our blog.

We don't have anything on numpy, but if you want to learn more about this module, there's a good quickstart tutorial page in the numpy documentation.

If you're interested in digging into the problems with floating point numbers, and a solution to those problems, you can check out our post comparing floats to decimal objects.