Welcome to day 21 of the 30 Days of Python series! Today we'll learn about how to (and why to) split our Python code into multiple files.

First though, we'll quickly recap how imports work in Python. Let's get started!

Recap of imports in Python

Doing a quick recap is worthwhile in this case, because it's important we understand a few points before going off and creating lots of files for our programs!

If you'd like something a little more comprehensive, feel free to re-read the day 18 post where we first covered imports.

Importing adds a name to globals()

If you have a Python file and you type this:

import json

print(globals())

Then that displays the names currently in the global namespace. The module name will be in the global namespace, ready for you to use.

Importing allows us to access elements of the imported module

After importing, we can do something like this to access something inside a module:

import math

print(math.pi)  # 3.14

We've accessed the pi name inside the math module. In this case, that is the value of the mathematical constant, pi.

We can alias imports to refer to them as a different name

Using the as keyword allows us to give the imported module a different name in our code. It doesn't change the name of the module we import—only the name we use in our code:

import math as m

print(m.pi)

I would generally discourage you from using aliasing, as using the full module name is often clearer when you read the code.

There are, however, many modules where using an aliased import is an established convention, such as using tk for tkinter, pd for pandas, and np for numpy.

We can import everything from a module using *

Although strongly discouraged most of the time, we can use * to add almost everything from a module to our global namespace.

from math import *

This can "pollute" our global namespace, filling it with variables. That can cause problems with conflicting names later on, and it also obscures where these names came from. For example, whenever you use something like pi in your code, it won't be obvious it has come from the math module.

Why split your code into files?

Now that we've quickly recapped the topic of imports, let's talk about why we may want to split our code into multiple files!

The first question we get when we start discussing this is: "isn't it easier to keep the code in one file?".

After all, you then don't have to deal with imports!

That may be true, but in programming ease of reading, maintainability, and the ability to share your code with others are much more important than ease of writing. Keeping your code in one file may be easier to write (to begin with), but as your code grows it can become impossible to work with.

Separating concerns and ease of organization

When we separate code into files, it's important we have a reason for putting some code in a certain file. Usually, we go by concerns. Code that does one thing goes into one file, and code that does something different goes into a different file.

For example, we might have one file for user interaction (prints and inputs), and another file for data storage (saving and retrieving things from a file).

At the moment the different concerns of a Python application may not be obvious to you. That's normal! Over time, you'll learn more about this. With more experience, it'll become clearer when things will benefit from getting split into files.

Separating files by concerns, assuming we give the files good names, also helps us find code more easily. If you have a file called data_storage.py, you're probably going to find code related to your file interactions there rather than in a user_menu.py file.

Using files (and folders) also does wonders for organisation. You can put files related to working with different types of data storage into one folder, for example.

Improved readability

As your files grow, they start getting more difficult to read. You may have many functions and variables, and finding things may require a lot of scrolling.

Modern editors have features to make it easier to find where things are defined, but this isn't a perfect solution.

Splitting your code into multiple files is a great way to produce smaller, more focused files. Navigating theses smaller files will be easier and so will understanding the content of each of these files.

Easier to reuse code

When you have multiple smaller, focused files, it's easier to reuse the contents of one file in multiple other files by importing.

A quick warning

It's also possible to have too many files! Don't aim to make your files as small as possible. Instead, make them focused.

If each file is concerned with one aspect of your application and every file has a good name, navigating your project will be easy!

Working with two files

Let's start off by going into a new Python project (or repl.it repl) and creating two files. Let's call them main.py and myfile.py.

Remember that repl.it will always run main.py when you press the "Run" button, as we mentioned in the day 0 post.

Because both files are in the same folder, you can import one from the other.

Let's type this in main.py:

import myfile

Note that we don't import myfile.py, but just myfile. Python does the rest!

What happens when you import

When we import a file, Python runs the file. It has to do that in order to determine what exists in that file.

Then it makes that file available to us in main.py by putting a reference to the module in the global namespace.

Let's add something into myfile.py:

print("Hello, world!")

Now let's import the file from main.py:

import myfile

print("What's going on?")

Try running that.

You'll see that we get two things printed out:

Hello, world!
What's going on?

That's because the myfile.py file was executed. It contains a print call, so therefore Python printed stuff out!

It's rare that you'll write code like this. Instead, the files you import will normally contain variables and functions, so that when you import them nothing actually happens until you actually use those variables and functions.

Let's add something a little more realistic to myfile.py. Maybe something like this:

def get_user_age():
    return int(input("Enter your age: "))

Now in main.py we can use the get_user_age function that we defined in myfile.py.

import myfile

try:
    myfile.get_user_age()
except ValueError:
    print("That's not a valid value for your age!")

Great!

That's the gist of it: separate things into files, and import them!

"Protected" names for files

Don't give your files the same names as built-in modules.

For example, if you create a new file and call it json.py, you'll get into trouble!

That's because if another file tries to import json, you won't be importing the built-in json module that lets us talk to JSON files. You'd be importing your json.py file instead!

Python always looks in the project folder for imports before looking at the built-in or installed packages.

Your files work in the same way as modules

Everything we could do with external modules, we can do with our own files:

  • Importing the whole file with import myfile and then referring to things as myfile.x.
  • Importing specific things with from myfile import x.
  • Aliased imports.
  • We can do from myfile mport * (although it's discouraged).

Using files and folders

You can create folders for your files if you think that'll help with organisation. In my experience, it usually does.

For example, let's create a folder in repl.it called user_interactions and move myfile.py into it. Now your file/folder structure will look like this:

- main.py
- user_interactions/
    | - myfile.py

From main.py you now have to use slightly different syntax to import myfile.py:

from user_interactions.myfile import get_user_age

try:
    get_user_age()
except ValueError:
    print("That's not a valid value for your age!")

When importing, the dot (.) means something like, "inside".

In the example above, we're therefore importing myfile from inside user_interactions.

If you have multiple sub-folders, you will need to use multiple . to separate the different levels of folders and files, like this:

from folder.subfolder.module import something_in_the_module

When you're importing like this, you'll either import specific things like we've done, or you'll use aliased imports. Either of these two are good (the first is generally better):

  • from user_interactions.myfile import get_user_age
  • import user_interactions.myfile as interactions

You normally won't be doing anything like in the example below, because it can get quite long to type out:

import user_interactions.myfile

user_interactions.myfile.get_user_age()

You need to refer to the complete import when you want to use something, so you'd be typing user_interactions.myfile a lot in your code!

It's OK to do that if you won't be using the file much though.

Script mode vs. module mode

When we run a file (e.g. in repl.it, that's main.py), we say that file is ran in "script mode".

When we import a file, that file runs in "module mode".

At the moment our project structure is like this:

- main.py
- user_interactions/
    | - myfile.py

I'll delete the contents of both files, and I'll place this in myfile.py:

print(__name__)

And I'll do this in main.py:

import user_interactions.myfile

print(__name__)

You'll see this output:

user_interactions.myfile
__main__

Remember that when we import, we run the file. Therefore the first line of output belongs to myfile.py, and the second line of output belongs to main.py.

The file that we run always has a __name__ variable with a value of "__main__". That is simply how Python tells us that we ran that file.

Any file that doesn't have a __name__ equal to "__main__" was imported.

Try moving things around and see how the output created by myfile.py changes!

Running code only in script mode

Sometimes we want to include some code in a file, but we only want that code to run if we executed that file directly—and not if we imported the file.

Since we know that __name__ must be equal to "__main__" for a file to have been run, we can use an if statement.

We could type this in myfile.py:

def get_user_age():
    return int(input("Enter your age: "))

if __name__ == "__main__":
    get_user_age()

That could allow us to run myfile.py (we can't do that in repl.it without some extra configuration), and see if the get_user_age() function works.

That is one of the key use cases of this construct: to help us see whether the stuff in a file works when we normally don't want it to run.

Another use case is for files which you don't normally run yourself. Sometimes you may write a file that is for use by another program, for example.

Using this construct would allow you to run your file for testing, while not affecting its functionality when it's imported by another program.

Exercises

For today's (only) exercise, we're giving you a bunch of code that is all in one file.

Access the code here.

Your task is to split that code into multiple files. You can choose how many and which files you want to split the code into, but think about why you're putting each piece on code in each file!

Then when you're done, check out our exercise solution to see how we'd split the code, and why.

Project

For today's project, we'll be using a Python library to create some graphs! As part of this project, you'll have to structure your code well so that it doesn't become an unreadable mess.

Check out the project brief for more information on today's project.