Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Installable Single Package

00:00 In the previous lesson, I talked about a simple package revolving around a single-file program. In this lesson, I’m going to make it more complicated and add more code and see how that changes the approach. First off, if you’ve got more than one coding file, you’re probably going to want to group it together into a module.

00:18 I’m going to take the example that I had before with a single file that had a Hello World in it and add a utilities file to go with it. When you have multiple files, typically that means you also want multiple test files.

00:30 You’ll also want to start doing things like checking your test coverage and running a linter to help you catch problematic code. The package itself doesn’t change by all that much. Here’s an example.

00:41 helloworld.py is now inside of a directory called helloworldthat’s the module. Because it’s a module, it has to have a __init__ file and the promised utils.py file that I talked about before, splitting up helloworld into multiple units.

00:58 Likewise, the tests now also becomes a directory. It’s a module as well so that it can be run. It also contains a __init__. There’s a specific file for testing the helloworld file, and a specific file for testing utils. runtests isn’t necessary, but it’s something I tend to do because I get tired of typing out the long python -m unittest command, and particularly if you start doing things like running coverage tests—which I’ll show you how to do—there’s a little more logic to it, so I create a little Bash file to help with that. The rest of the files here that I’ve grayed out are the same files that were there in the previous package.

01:36 This is my new version of Hello World. It’s very, very similar to the previous one. All that’s changed is I’m now importing code from the utils library inside of helloworld. So, I’m now using a module helloworld with a utils file inside of it and grabbing the function show_message().

01:52 Then, I’m replacing what was a print() function on line 14 with the show_message() function. show_message() is just a wrapper for print()utils is a really simple file—but you can see how the two files interact inside of the module together.

02:06 What was formerly tests.py has been moved inside of a tests module and it has been renamed to TestHelloWorld to match with the helloworld.py function.

02:16 This changes how things are imported, so line 8 has changed: importing the do_hello and URL from the file that’s been moved. The other thing that has changed in this file is there’s some stuff missing.

02:29 Because I’m going to be using the runtests and coveragewhich I’ll show you in a minute—I no longer need the '__main__' section at the bottom of the file for execution. The last thing I changed was—you may recall—helloworld used to have a __version__ variable inside of it.

02:44 I’ve moved this into the __init__ file—this makes it easier to grab and will simplify some of the code inside of setup.py, which I’ll show you in a few minutes.

02:56 This entire lesson is on writing more code with a larger module, which means you really should be writing more tests to go with it. Once you’re writing a lot of tests, it’s a good idea to figure out whether or not your tests are actually doing their job.

03:08 One measure of this is the coverage. Coverage tells you what percentage of your code has actually been executed by the tests. There’s some great libraries out there to help you with this; one of the most popular ones is simply enough called coverage, and it can be found by installing pip install coverage.

03:29 Here’s some sample output from the coverage report. This particular example is where I’ve commented out some of the test code so that I don’t get 100% coverage.

03:37 The first line here is the output from the program. I’ve actually commented out the line that does something, which is why this is empty. The second section is Python telling me that the tests have run.

03:48 One of my tests is empty, but of course, it passes when it’s empty. And the final section here is the simple report from coverage. You can see helloworld.py was executed, two of the lines inside of it were missed and not run, and it only has 75% coverage.

04:04 You can get more information by running the coverage command with the html argument to get a full report inside of your browser.

04:12 My little script here actually reminds me that this is what the command is for that. I’ll show you an example.

04:19 And here it is—a code-highlighted version of the helloworld.py. It highlights what was run, what’s missing, and anything that’s been excluded.

04:28 It’s very hard to test the __main__ execution of a script inside of your tests files, so instead of measuring it, I test the do_hello() function specifically and then do a # pragma: no cover so that coverage doesn’t actually examine this line.

04:43 This is the Bash script that I was talking about earlier that actually runs the coverage command. First off, I do a quick find and remove any .pyc files.

04:52 This stops anything from being cached and causing strange behaviors. This is overkill, but it’s saved my bacon on a couple occasions. The actual coverage command is very similar to doing the -m unittest with Python directly, except that you also pass in information on what modules to look for source code in. This line itself is sufficient to get the coverage information, but then as you’ve seen inside of the results, I create the simple coverage report and then echo some of this information out in an easier-to-read fashion.

05:24 If you’re using a linter like pyflakes or pep8, this is a good place to put it so that it runs after you’ve run your tests every single time. The last thing that I want to show you in this lesson is just some simplification inside of setup.py. In the previous version, there were four or five lines that read in the helloworld.py file and grepped out the __version__ value.

05:46 Now that __version__ is put inside of __init__, it can very easily be imported directly, and this removes four or five lines from the setup.py.

05:55 So, this is just a little bit easier.

05:58 This lesson talked about the file structure changes in going from a single-file program to a multi-file module. In the next lesson, I’m going to make things even more complicated and add multiple modules.

Avatar image for Peter

Peter on May 7, 2020

Outdated information: Python modules don’t have to have init.py files since Python 3.3, released in 2012.

Avatar image for Dan Bader

Dan Bader RP Team on May 13, 2020

@Peter: That is not true, please see this section in the official Python 3.8 docs for example:

The __init__.py files are required to make Python treat directories containing the file as packages.

Source: docs.python.org/3.8/tutorial/modules.html#packages

Become a Member to join the conversation.