Chapter 7. Testing, Profiling, and Dealing with Exceptions
"Code without tests is broken by design." | ||
--Jacob Kaplan-Moss |
Jacob Kaplan-Moss is one of the core developers of the Django web framework. We're going to explore it in the next chapters. I strongly agree with this quote of his. I believe code without tests shouldn't be deployed to production.
Why are tests so important? Well, for one, they give you predictability. Or, at least, they help you achieve high predictability. Unfortunately, there is always some bug that sneaks into our code. But we definitely want our code to be as predictable as possible. What we don't want is to have a surprise, our code behaving in an unpredictable way. Would you be happy to know that the software that checks on the sensors of the plane that is taking you on holidays sometimes goes crazy? No, probably not.
Therefore we need to test our code, we need to check that its behavior is correct, that it works as expected when it deals with edge cases, that it doesn't hang when the components it's talking to are down, that the performances are well within the acceptable range, and so on.
This chapter is all about this topic, making sure that your code is prepared to face the scary outside world, that is fast enough and that it can deal with unexpected or exceptional conditions.
We're going to explore testing, including a brief introduction to test-driven development (TDD), which is one of my favorite working methodologies. Then, we're going to explore the world of exceptions, and finally we're going to talk a little bit about performances and profiling. Deep breath, and here we go...
Testing your application
There are many different kinds of tests, so many in fact that companies often have a dedicated department, called quality assurance (QA), made up of individuals that spend their day testing the software the company developers produce.
To start making an initial classification, we can divide tests into two broad categories: white-box and black-box tests.
White-box tests are those which exercise the internals of the code, they inspect it down to a very fine level of granularity. On the other hand, black-box tests are those which consider the software under testing as if being within a box, the internals of which are ignored. Even the technology, or the language used inside the box is not important for black-box tests. What they do is to plug input to one end of the box and verify the output at the other end, and that's it.
Note
There is also an in-between category, called gray-box testing, that involves testing a system in the same way we do with the black-box approach, but having some knowledge about the algorithms and data structures used to write the software and only partial access to its source code.
There are many different kinds of tests in these categories, each of which serves a different purpose. Just to give you an idea, here's a few:
- Front-end tests make sure that the client side of your application is exposing the information that it should, all the links, the buttons, the advertising, everything that needs to be shown to the client. It may also verify that it is possible to walk a certain path through the user interface.
- Scenario tests make use of stories (or scenarios) that help the tester work through a complex problem or test a part of the system.
- Integration tests verify the behavior of the various components of your application when they are working together sending messages through interfaces.
- Smoke tests are particularly useful when you deploy a new update on your application. They check whether the most essential, vital parts of your application are still working as they should and that they are not on fire. This term comes from when engineers tested circuits by making sure nothing was smoking.
- Acceptance tests, or user acceptance testing (UAT) is what a developer does with a product owner (for example, in a SCRUM environment) to determine if the work that was commissioned was carried out correctly.
- Functional tests verify the features or functionalities of your software.
- Destructive tests take down parts of your system, simulating a failure, in order to establish how well the remaining parts of the system perform. These kinds of tests are performed extensively by companies that need to provide an extremely reliable service, such as Amazon, for example.
- Performance tests aim to verify how well the system performs under a specific load of data or traffic so that, for example, engineers can get a better understanding of which are the bottlenecks in the system that could bring it down to its knees in a heavy load situation, or those which prevent scalability.
- Usability tests, and the closely related user experience (UX) tests, aim to check if the user interface is simple and easy to understand and use. They aim to provide input to the designers so that the user experience is improved.
- Security and penetration tests aim to verify how well the system is protected against attacks and intrusions.
- Unit tests help the developer to write the code in a robust and consistent way, providing the first line of feedback and defense against coding mistakes, refactoring mistakes, and so on.
- Regression tests provide the developer with useful information about a feature being compromised in the system after an update. Some of the causes for a system being said to have a regression are an old bug coming back to life, an existing feature being compromised, or a new issue being introduced.
Many books and articles have been written about testing, and I have to point you to those resources if you're interested in finding out more about all the different kinds of tests. In this chapter, we will concentrate on unit tests, since they are the backbone of software crafting and form the vast majority of tests that are written by a developer.
Testing is an art, an art that you don't learn from books, I'm afraid. You can learn all the definitions (and you should), and try and collect as much knowledge about testing as you can but I promise you, you will be able to test your software properly only when you have done it for long enough in the field.
When you are having trouble refactoring a bit of code, because every little thing you touch makes a test blow up, you learn how to write less rigid and limiting tests, which still verify the correctness of your code but, at the same time, allow you the freedom and joy to play with it, to shape it as you want.
When you are being called too often to fix unexpected bugs in your code, you learn how to write tests more thoroughly, how to come up with a more comprehensive list of edge cases, and strategies to cope with them before they turn into bugs.
When you are spending too much time reading tests and trying to refactor them in order to change a small feature in the code, you learn to write simpler, shorter, and better focused tests.
I could go on with this when you... you learn..., but I guess you get the picture. You need to get your hands dirty and build experience. My suggestion? Study the theory as much as you can, and then experiment using different approaches. Also, try to learn from experienced coders; it's very effective.
The anatomy of a test
Before we concentrate on unit tests, let's see what a test is, and what its purpose is.
A test is a piece of code whose purpose is to verify something in our system. It may be that we're calling a function passing two integers, that an object has a property called donald_duck
, or that when you place an order on some API, after a minute you can see it dissected into its basic elements, in the database.
A test is typically comprised of three sections:
- Preparation: This is where you set up the scene. You prepare all the data, the objects, the services you need in the places you need them so that they are ready to be used.
- Execution: This is where you execute the bit of logic that you're checking against. You perform an action using the data and the interfaces you have set up in the preparation phase.
- Verification: This is where you verify the results and make sure they are according to your expectations. You check the returned value of a function, or that some data is in the database, some is not, some has changed, a request has been made, something has happened, a method has been called, and so on.
Testing guidelines
Like software, tests can be good or bad, with the whole range of shades in the middle. In order to write good tests, here are some guidelines:
- Keep them as simple as possible: It's okay to violate some good coding rules, such as hardcoding values or duplicating code. Tests need first and foremost to be as readable as possible and easy to understand. When tests are hard to read or understand, you can never be sure if they are actually making sure your code is performing correctly.
- Tests should verify one thing and one thing only: It's very important that you keep them short and contained. It's perfectly fine to write multiple tests to exercise a single object or function. Just make sure that each test has one and only one purpose.
- Tests should not make any unnecessary assumption when verifying data: This is tricky to understand at first, but say you are testing the return value of a function and it is an unordered list of numbers (like
[2, 3, 1]
). If the order in that list is random, in the test you may be tempted to sort it and compare it with[1, 2, 3]
. If you do, you will introduce an extra assumption on the ordering of the result of your function call, and this is bad practice. You should always find a way to verify things without introducing any assumptions or any feature that doesn't belong in the use case you're describing with your test. - Tests should exercise the what, rather than the how: Tests should focus on checking what a function is supposed to do, rather than how it is doing it. For example, focus on the fact that it's calculating the square root of a number (the what), instead of on the fact that it is calling
math.sqrt
to do it (the how). Unless you're writing performance tests or you have a particular need to verify how a certain action is performed, try to avoid this type of testing and focus on the what. Testing the how leads to restrictive tests and makes refactoring hard. Moreover, the type of test you have to write when you concentrate on the how is more likely to degrade the quality of your testing code base when you amend your software frequently (more on this later). - Tests should assume the least possible in the preparation phase: Say you have 10 tests that are checking how a data structure is manipulated by a function. And let's say this data structure is a dict with five key/value pairs. If you put the complete dict in each test, the moment you have to change something in that dict, you also have to amend all ten tests. On the other hand, if you strip down the test data as much as you can, you will find that, most of the time, it's possible to have the majority of tests checking only a partial version of the data, and only a few running with a full version of it. This means that when you need to change your data, you will have to amend only those tests that are actually exercising it.
- Test should run as fast as possible: A good test codebase could end up being much longer than the code being tested itself. It varies according to the situation and the developer but whatever the length, you'll end up having hundreds, if not thousands, of tests to run, which means the faster they run, the faster you can get back to writing code. When using TDD, for example, you run tests very often, so speed is essential.
- Tests should use up the least possible amount of resources: The reason for this is that every developer who checks out your code should be able to run your tests, no matter how powerful their box is. It could be a skinny virtual machine or a neglected Jenkins box, your tests should run without chewing up too many resources.
Note
A Jenkins box is a machine that runs Jenkins, software that is capable of, amongst many other things, running your tests automatically. Jenkins is frequently used in companies where developers use practices like continuous integration, extreme programming, and so on.
Unit testing
Now that you have an idea about what testing is and why we need it, let's finally introduce the developer's best friend: the unit test.
Before we proceed with the examples, allow me to spend some words of caution: I'll try to give you the fundamentals about unit testing, but I don't follow any particular school of thought or methodology to the letter. Over the years, I have tried many different testing approaches, eventually coming up with my own way of doing things, which is constantly evolving. To put it as Bruce Lee would have:
"Absorb what is useful, discard what is useless and add what is specifically your own".
Writing a unit test
In order to explain how to write a unit test, let's help ourselves with a simple snippet:
data.py
def get_clean_data(source): data = load_data(source) cleaned_data = clean_data(data) return cleaned_data
The function get_clean_data
is responsible for getting data from source
, cleaning it, and returning it to the caller. How do we test this function?
One way of doing this is to call it and then make sure that load_data
was called once with source
as its only argument. Then we have to verify that clean_data
was called once, with the return value of load_data
. And, finally, we would need to make sure that the return value of clean_data
is what is returned by the get_clean_data
function as well.
In order to do this, we need to set up the source and run this code, and this may be a problem. One of the golden rules of unit testing is that anything that crosses the boundaries of your application needs to be simulated. We don't want to talk to a real data source, and we don't want to actually run real functions if they are communicating with anything that is not contained in our application. A few examples would be a database, a search service, an external API, a file in the filesystem, and so on.
We need these restrictions to act as a shield, so that we can always run our tests safely without the fear of destroying something in a real data source.
Another reason is that it may be quite difficult for a single developer to reproduce the whole architecture on their box. It may require the setting up of databases, APIs, services, files and folders, and so on and so forth, and this can be difficult, time consuming, or sometimes not even possible.
Note
Very simply put, an application programming interface (API) is a set of tools for building software applications. An API expresses a software component in terms of its operations, inputs and outputs, and underlying types. For example, if you create a software that needs to interface with a data provider service, it's very likely that you will have to go through their API in order to gain access to the data.
Therefore, in our unit tests, we need to simulate all those things in some way. Unit tests need to be run by any developer without the need for the whole system to be set up on their box.
A different approach, which I always favor when it's possible to do so, is to simulate entities without using fake objects, but using special purpose test objects instead. For example, if your code talks to a database, instead of faking all the functions and methods that talk to the database and programming the fake objects so that they return what the real ones would, I'd much rather prefer to spawn a test database, set up the tables and data I need, and then patch the connection settings so that my tests are running real code, against the test database, thereby doing no harm at all. In-memory databases are excellent options for these cases.
Note
One of the applications that allow you to spawn a database for testing, is Django. Within the django.test
package you can find several tools that help you write your tests so that you won't have to simulate the dialog with a database. By writing tests this way, you will also be able to check on transactions, encodings, and all other database related aspects of programming. Another advantage of this approach consists in the ability of checking against things that can change from one database to another.
Sometimes, though, it's still not possible, and we need to use fakes, therefore let's talk about them.
Mock objects and patching
First of all, in Python, these fake objects are called mocks. Up to version 3.3, the mock
library was a third-party library that basically every project would install via pip
but, from version 3.3, it has been included in the standard library under the unittest
module, and rightfully so, given its importance and how widespread it is.
The act of replacing a real object or function (or in general, any piece of data structure) with a mock, is called patching. The mock
library provides the patch
tool, which can act as a function or class decorator, and even as a context manager (more on this in Chapter 8, The Edges – GUIs and Scripts), that you can use to mock things out. Once you have replaced everything you need not to run, with suitable mocks, you can pass to the second phase of the test and run the code you are exercising. After the execution, you will be able to check those mocks to verify that your code has worked correctly.
Assertions
The verification phase is done through the use of assertions. An assertion is a function (or method) that you can use to verify equality between objects, as well as other conditions. When a condition is not met, the assertion will raise an exception that will make your test fail. You can find a list of assertions in the unittest
module documentation, and their corresponding Pythonic version in the nose third-party library, which provides a few advantages over the sheer unittest
module, starting from an improved test discovery strategy (which is the way a test runner detects and discovers the tests in your application).
A classic unit test example
Mocks, patches, and assertions are the basic tools we'll be using to write tests. So, finally, let's see an example. I'm going to write a function that takes a list of integers and filters out all those which aren't positive.
filter_funcs.py
def filter_ints(v): return [num for num in v if is_positive(num)] def is_positive(n): return n > 0
In the preceding example, we define the filter_ints
function, which basically uses a list comprehension to retain all the numbers in v
that are positive, discarding zeros and negative ones. I hope, by now, any further explanation of the code would be insulting.
What is interesting, though, is to start thinking about how we can test it. Well, how about we call filter_ints
with a list of numbers and we make sure that is_positive
is called for each of them? Of course, we would have to test is_positive
as well, but I will show you later on how to do that. Let's write a simple test for filter_ints
now.
Note
Just to be sure we're on the same page, I am putting the code for this chapter in a folder called ch7
, which lies within the root of our project. At the same level of ch7
, I have created a folder called tests
, in which I have placed a folder called test_ch7
. In this folder I have one test file, called test_filter_func.py
.
Basically, within the tests
folder, I will recreate the tree structure of the code I'm testing, prepending everything with test_
. This way, finding tests is really easy, as well as is keeping them tidy.
tests/test_ch7/test_filter_funcs.py
from unittest import TestCase # 1 from unittest.mock import patch, call # 2 from nose.tools import assert_equal # 3 from ch7.filter_funcs import filter_ints # 4 class FilterIntsTestCase(TestCase): # 5 @patch('ch7.filter_funcs.is_positive') # 6 def test_filter_ints(self, is_positive_mock): # 7 # preparation v = [3, -4, 0, 5, 8] # execution filter_ints(v) # 8 # verification assert_equal( [call(3), call(-4), call(0), call(5), call(8)], is_positive_mock.call_args_list ) # 9
My, oh my, so little code, and yet so much to say. First of all: #1
. The TestCase
class is the base class that we use to have a contained entity in which to run our tests. It's not just a bare container; it provides you with methods to write tests more easily.
On #2
, we import patch
and call
from the unittest.mock
module. patch
is responsible for substituting an object with a Mock
instance, thereby giving us the ability to check on it after the execution phase has been completed. call
provides us with a nice way of expressing a (for example, function) call.
On #3
, you can see that I prefer to use assertions from nose
, rather than the ones that come with the unittest
module. To give you an example, assert_equal(...)
would become self.assertEqual(...)
if I didn't use nose
. I don't enjoy typing self.
for any assertion, if there is a way to avoid it, and I don't particularly enjoy camel case, therefore I always prefer to use nose
to make my assertions.
assert_equal
is a function that takes two parameters (and an optional third one that acts as a message) and verifies that they are the same. If they are equal, nothing happens, but if they differ, then an AssertionError
exception is raised, telling us something is wrong. When I write my tests, I always put the expected value as the first argument, and the real one as the second. This convention saves me time when I'm reading tests.
On #4
, we import the function we want to test, and then (#5
) we proceed to create the class where our tests will live. Each method of this class starting with test_
, will be interpreted as a test. As you can see, we need to decorate test_filter_ints
with patch
(#6
). Understanding this part is crucial, we need to patch the object where it is actually used. In this case, the path is very simple: ch7.filter_func.is_positive
.
Tip
Patching can be very tricky, so I urge you to read the Where to patch section in the mock documentation: https://docs.python.org/3/library/unittest.mock.html#where-to-patch.
When we decorate a function using patch
, like in our example, we get an extra argument in the test signature (#7
), which I like to call as the patched function name, plus a _mock
suffix, just to make it clear that the object has been patched (or mocked out).).
Finally, we get to the body of the test, and we have a very simple preparation phase in which we set up a list with at least one representative of all the integer number categories (negative, zero, and positive).
Then, in #8
, we perform the execution phase, which runs the filter_ints
function, without collecting its results. If all has gone as expected, the fake is_positive
function must have been called with each of the integers in v
.
We can verify this by comparing a list of call objects to the call_args_list
attribute on the mock (#9
). This attribute is the list of all the calls performed on the object since its creation.
Let's run this test. First of all, make sure that you install nose
($ pip freeze
will tell you if you have it already):
$ pip install nose
Then, change into the root of the project (mine is called learning.python
), and run the tests like this:
$ nosetests tests/test_ch7/ . ------------------------------------------------------------ Ran 1 test in 0.006s OK
The output shows one dot (each dot is a test), a separation line, and the time taken to run the whole test suite. It also says OK
at the end, which means that our tests were all successful.
Making a test fail
Good, so just for fun let's make one fail. In the test file, change the last call from call(8)
to call(9)
, and run the tests again:
$ nosetests tests/test_ch7/ F ============================================================ FAIL: test_filter_ints (test_filter_funcs.FilterIntsTestCase) ------------------------------------------------------------ Traceback (most recent call last): File "/usr/lib/python3.4/unittest/mock.py", line 1125, in patched return func(*args, **keywargs) File "/home/fab/srv/learning.python/tests/test_ch7/test_filter_funcs.py", line 21, in test_filter_ints is_positive_mock.call_args_list AssertionError: [call(3), call(-4), call(0), call(5), call(9)] != [call(3), call(-4), call(0), call(5), call(8)] ------------------------------------------------------------ Ran 1 test in 0.008s FAILED (failures=1)
Wow, we made the beast angry! So much wonderful information, though. This tells you that the test test_filter_ints
(with the path to it), was run and that it failed (the big F
at the top, where the dot was before). It gives you a Traceback
, that tells you that in the test_filter_funcs.py
module, at line 21, when asserting on is_positive_mock.call_args_list
, we have a discrepancy. The test expects the list of calls to end with a call(9)
instance, but the real list ends with a call(8)
. This is nothing less than wonderful.
If you have a test like this, can you imagine what would happen if you refactored and introduced a bug into your function by mistake? Well, your tests will break! They will tell you that you have screwed something up, and here's the details. So, you go and check out what you broke.
Interface testing
Let's add another test that checks on the returned value. It's another method in the class, so I won't reproduce the whole code again:
tests/test_ch7/test_filter_funcs.py
def test_filter_ints_return_value(self): v = [3, -4, 0, -2, 5, 0, 8, -1] result = filter_ints(v) assert_list_equal([3, 5, 8], result)
This test is a bit different from the previous one. Firstly, we cannot mock the is_positive
function, otherwise we wouldn't be able to check on the result. Secondly, we don't check on calls, but only on the result of the function when input is given.
I like this test much more than the previous one. This type of test is called an interface test because it checks on the interface (the set of inputs and outputs) of the function we're testing. It doesn't use any mocks, which is why I use this technique much more than the previous one. Let's run the new test suite and then let's see why I like interface testing more than those with mocks.
$ nosetests tests/test_ch7/ .. ------------------------------------------------------------ Ran 2 tests in 0.006s OK
Two tests ran, all good (I changed that 9
back to an 8
in the first test, of course).
Comparing tests with and without mocks
Now, let's see why I don't really like mocks and use them only when I have no choice. Let's refactor the code in this way:
filter_funcs_refactored.py
def filter_ints(v): v = [num for num in v if num != 0] # 1 return [num for num in v if is_positive(num)]
The code for is_positive
is the same as before. But the logic in filter_ints
has now changed in a way that is_positive
will never be called with a 0
, since they are all filtered out in #1
. This leads to an interesting result, so let's run the tests again:
$ nosetests tests/test_ch7/test_filter_funcs_refactored.py F. ============================================================ FAIL: test_filter_ints (test_filter_funcs_refactored.FilterIntsTestCase) ------------------------------------------------------------ ... omit ... AssertionError: [call(3), call(-4), call(0), call(5), call(8)] != [call(3), call(-4), call(5), call(8)] ------------------------------------------------------------ Ran 2 tests in 0.002s FAILED (failures=1)
One test succeeded but the other one, the one with the mocked is_positive
function, failed. The AssertionError
message shows us that we now need to amend the list of expected calls, removing call(0)
, because it is no longer performed.
This is not good. We have changed neither the interface of the function nor its behavior. The function is still keeping to its original contract. What we've done by testing it with a mocked object is limit ourselves. In fact, we now have to amend the test in order to use the new logic.
This is just a simple example but it shows one important flaw in the whole mock mechanism. You must keep your mocks up-to-date and in sync with the code they are replacing, otherwise you risk having issues like the preceding one, or even worse. Your tests may not fail because they are using mocked objects that perform fine, but because the real ones, now not in sync any more, are actually failing.
So use mocks only when necessary, only when there is no other way of testing your functions. When you cross the boundaries of your application in a test, try to use a replacement, like a test database, or a fake API, and only when it's not possible, resort to mocks. They are very powerful, but also very dangerous when not handled properly.
So, let's remove that first test and keep only the second one, so that I can show you another issue you could run into when writing tests. The whole test module now looks like this:
tests/test_ch7/test_filter_funcs_final.py
from unittest import TestCase from nose.tools import assert_list_equal from ch7.filter_funcs import filter_ints class FilterIntsTestCase(TestCase): def test_filter_ints_return_value(self): v = [3, -4, 0, -2, 5, 0, 8, -1] result = filter_ints(v) assert_list_equal([3, 5, 8], result)
If we run it, it will pass.
A brief chat about triangulation. Now let me ask you: what happens if I change my filter_ints
function to this?
filter_funcs_triangulation.py
def filter_ints(v): return [3, 5, 8]
If you run the test suite, the test we have will still pass! You may think I'm crazy but I'm showing you this because I want to talk about a concept called triangulation, which is very important when doing interface testing with TDD.
The whole idea is to remove cheating code, or badly performing code, by pinpointing it from different angles (like going to one vertex of a triangle from the other two) in a way that makes it impossible for our code to cheat, and the bug is exposed. We can simply modify the test like this:
tests/test_ch7/test_filter_funcs_final_triangulation.py
def test_filter_ints_return_value(self): v1 = [3, -4, 0, -2, 5, 0, 8, -1] v2 = [7, -3, 0, 0, 9, 1] assert_list_equal([3, 5, 8], filter_ints(v1)) assert_list_equal([7, 9, 1], filter_ints(v2))
I have moved the execution section in the assertions directly, and you can see that we're now pinpointing our function from two different angles, thereby requiring that the real code be in it. It's no longer possible for our function to cheat.
Triangulation is a very powerful technique that teaches us to always try to exercise our code from many different angles, to cover all possible edge cases to expose any problems.
Boundaries and granularity
Let's now add a test for the is_positive
function. I know it's a one-liner, but it presents us with opportunity to discuss two very important concepts: boundaries and granularity.
That 0
in the body of the function is a boundary, the >
in the inequality is how we behave with regards to this boundary. Typically, when you set a boundary, you divide the space into three areas: what lies before the boundary, after the boundary, and on the boundary itself. In the example, before the boundary we find the negative numbers, the boundary is the element 0
and, after the boundary, we find the positive numbers. We need to test each of these areas to be sure we're testing the function correctly. So, let's see one possible solution (I will add the test to the class, but I won't show the repeated code):
tests/test_ch7/test_filter_funcs_is_positive_loose.py
def test_is_positive(self): assert_equal(False, is_positive(-2)) # before boundary assert_equal(False, is_positive(0)) # on the boundary assert_equal(True, is_positive(2)) # after the boundary
You can see that we are exercising one number for each different area around the boundary. Do you think this test is good? Think about it for a minute before reading on.
The answer is no, this test is not good. Not good enough, anyway. If I change the body of the is_positive
function to read return n > 1
, I would expect my test to fail, but it won't. -2
is still False
, as well as 0
, and 2
is still True
. Why does that happen? It is because we haven't taken granularity properly into account. We're dealing with integers, so what is the minimum granularity when we move from one integer to the next one? It's 1. Therefore, when we surround the boundary, taking all three areas into account is not enough. We need to do it with the minimum possible granularity. Let's change the test:
tests/test_ch7/test_filter_funcs_is_positive_correct.py
def test_is_positive(self): assert_equal(False, is_positive(-1)) assert_equal(False, is_positive(0)) assert_equal(True, is_positive(1))
Ah, now it's better. Now if we change the body of is_positive
to read return n > 1
, the third assertion will fail, which is what we want. Can you think of a better test?
tests/test_ch7/test_filter_funcs_is_positive_better.py
def test_is_positive(self): assert_equal(False, is_positive(0)) for n in range(1, 10 ** 4): assert_equal(False, is_positive(-n)) assert_equal(True, is_positive(n))
This test is even better. We test the first ten thousand integers (both positive and negative) and 0. It basically provides us with a better coverage than just the one across the boundary. So, keep this in mind. Zoom closely around each boundary with minimal granularity, but try to expand as well, finding a good compromise between optimal coverage and execution speed. We would love to check the first billion integers, but we can't wait days for our tests to run.
A more interesting example
Okay, this was as gentle an introduction as I could give you, so let's move on to something more interesting. Let's write and test a function that flattens a nested dictionary structure. For a couple of years, I have worked very closely with Twitter and Facebook APIs. Handling such humongous data structures is not easy, especially since they're often deeply nested. It turns out that it's much easier to flatten them in a way that you can work on them without losing the original structural information, and then recreate the nested structure from the flat one. To give you an example, we want something like this:
data_flatten.py
nested = { 'fullname': 'Alessandra', 'age': 41, 'phone-numbers': ['+447421234567', '+447423456789'], 'residence': { 'address': { 'first-line': 'Alexandra Rd', 'second-line': '', }, 'zip': 'N8 0PP', 'city': 'London', 'country': 'UK', }, } flat = { 'fullname': 'Alessandra', 'age': 41, 'phone-numbers': ['+447421234567', '+447423456789'], 'residence.address.first-line': 'Alexandra Rd', 'residence.address.second-line': '', 'residence.zip': 'N8 0PP', 'residence.city': 'London', 'residence.country': 'UK', }
A structure like flat
is much simpler to manipulate. Before writing the flattener, let's make some assumptions: the keys are strings, we leave every data structure as it is unless it's a dictionary, in which case we flatten it, we use the dot as separator, but we want to be able to pass a different one to our function. Here's the code:
data_flatten.py
def flatten(data, prefix='', separator='.'): """Flattens a nested dict structure. """ if not isinstance(data, dict): return {prefix: data} if prefix else data result = {} for (key, value) in data.items(): result.update( flatten( value, _get_new_prefix(prefix, key, separator), separator=separator)) return result def _get_new_prefix(prefix, key, separator): return (separator.join((prefix, str(key))) if prefix else str(key))
The preceding example is not difficult, but also not trivial so let's go through it. At first, we check if data
is a dictionary. If it's not a dictionary, then it's data that doesn't need to be flattened; therefore, we simply return either data
or, if prefix
is not an empty string, a dictionary with one key/value pair: prefix
/data
.
If instead data
is a dict, we prepare an empty result
dict to return, then we parse the list of data
's items, which, at I'm sure you will remember, are 2-tuples (key, value). For each (key, value) pair, we recursively call flatten
on them, and we update the result
dict with what's returned by that call. Recursion is excellent when running through nested structures.
At a glance, can you understand what the _get_new_prefix
function does? Let's use the inside-out technique once again. I see a ternary operator that returns the stringified key
when prefix
is an empty string. On the other hand, when prefix
is a non-empty string, we use the separator
to join
the prefix
with the stringified version of key
. Notice that the braces inside the call to join
aren't redundant, we need them. Can you figure out why?
Let's write a couple of tests for this function:
tests/test_ch7/test_data_flatten.py
# ... imports omitted ... class FlattenTestCase(TestCase): def test_flatten(self): test_cases = [ ({'A': {'B': 'C', 'D': [1, 2, 3], 'E': {'F': 'G'}}, 'H': 3.14, 'J': ['K', 'L'], 'M': 'N'}, {'A.B': 'C', 'A.D': [1, 2, 3], 'A.E.F': 'G', 'H': 3.14, 'J': ['K', 'L'], 'M': 'N'}), (0, 0), ('Hello', 'Hello'), ({'A': None}, {'A': None}), ] for (nested, flat) in test_cases: assert_equal(flat, flatten(nested)) def test_flatten_custom_separator(self): nested = {'A': {'B': {'C': 'D'}}} assert_equal( {'A#B#C': 'D'}, flatten(nested, separator='#'))
Let's start from test_flatten
. I defined a list of 2-tuples (nested, flat)
, each of which represents a test case (I highlighted nested
to ease reading). I have one big dict with three levels of nesting, and then some smaller data structures that won't change when passed to the flatten
function. These test cases are probably not enough to cover all edge cases, but they should give you a good idea of how you could structure a test like this. With a simple for
loop, I cycle through each test case and assert that the result of flatten(nested)
is equal to flat
.
Tip
One thing to say about this example is that, when you run it, it will show you that two tests have been run. This is actually not correct because even if technically there were only two tests running, in one of them we have multiple test cases. It would be nicer to have them run in a way that they were recognized as separate. This is possible through the use of libraries such as nose-parameterized
, which I encourage you to check out. It's on https://pypi.python.org/pypi/nose-parameterized.
I also provided a second test to make sure the custom separator feature worked. As you can see, I used only one data structure, which is much smaller. We don't need to go big again, nor to test other edge cases. Remember, tests should make sure of one thing and one thing only, and test_flatten_custom_separator
just takes care of verifying whether or not we can feed the flatten
function a different separator
.
I could keep blathering on about tests for about another book if only I had the space, but unfortunately, we need to stop here. I haven't told you about doctests (tests written in the documentation using a Python interactive shell style), and about another half a million things that could be said about this subject. You'll have to discover that for yourself.
Take a look at the documentation for the unittest
module, the nose
and nose-parameterized
libraries, and pytest
(http://pytest.org/), and you will be fine. In my experience, mocking and patching seem to be quite hard to get a good grasp of for developers who are new to them, so allow yourself a little time to digest these techniques. Try and learn them gradually.
Test-driven development
Let's talk briefly about test-driven development or TDD. It is a methodology that was rediscovered by Kent Beck, who wrote Test Driven Development by Example, Addison Wesley – 2002, which I encourage you to check out if you want to learn about the fundamentals of this subject, which I'm quite obsessed with.
TDD is a software development methodology that is based on the continuous repetition of a very short development cycle.
At first, the developer writes a test, and makes it run. The test is supposed to check a feature that is not yet part of the code. Maybe is a new feature to be added, or something to be removed or amended. Running the test will make it fail and, because of this, this phase is called Red.
When the test has failed, the developer writes the minimal amount of code to make it pass. When running the test succeeds, we have the so-called Green phase. In this phase, it is okay to write code that cheats, just to make the test pass (that's why you would then use triangulation). This technique is called, fake it 'til you make it.
The last piece of this cycle is where the developer takes care of both the code and the tests (in separate times) and refactors them until they are in the desired state. This last phase is called Refactor.
The TDD mantra therefore recites, Red-Green-Refactor.
At first, it feels really weird to write tests before the code, and I must confess it took me a while to get used to it. If you stick to it, though, and force yourself to learn this slightly counter-intuitive way of working, at some point something almost magical happens, and you will see the quality of your code increase in a way that wouldn't be possible otherwise.
When you write your code before the tests, you have to take care of what the code has to do and how it has to do it, both at the same time. On the other hand, when you write tests before the code, you can concentrate on the what part alone, while you write them. When you write the code afterwards, you will mostly have to take care of how the code has to implement what is required by the tests. This shift in focus allows your mind to concentrate on the what and how parts in separate moments, yielding a brain power boost that will surprise you.
There are several other benefits that come from the adoption of this technique:
- You will refactor with much more confidence: Because when you touch your code you know that if you screw things up, you will break at least one test. Moreover, you will be able to take care of the architectural design in the refactor phase, where having tests that act as guardians will allow you to enjoy massaging the code until it reaches a state that satisfies you.
- The code will be more readable: This is crucial in our time, when coding is a social activity and every professional developer spends much more time reading code than writing it.
- The code will be more loose-coupled and easier to test and maintain: This is simply because writing the tests first forces you to think more deeply about its structure.
- Writing tests first requires you to have a better understanding of the business requirements: This is fundamental in delivering what was actually asked for. If your understanding of the requirements is lacking information, you'll find writing a test extremely challenging and this situation acts as a sentinel for you.
- Having everything unit tested means the code will be easier to debug: Moreover, small tests are perfect for providing alternative documentation. English can be misleading, but five lines of Python in a simple test are very hard to be misunderstood.
- Higher speed: It's faster to write tests and code than it is to write the code first and then lose time debugging it. If you don't write tests, you will probably deliver the code sooner, but then you will have to track the bugs down and solve them (and, rest assured, there will be bugs). The combined time taken to write the code and then debug it is usually longer than the time taken to develop the code with TDD, where having tests running before the code is written, ensuring that the amount of bugs in it will be much lower than in the other case.
On the other hand, the main shortcomings of this technique are:
- The whole company needs to believe in it: Otherwise you will have to constantly argue with your boss, who will not understand why it takes you so long to deliver. The truth is, it may take you a bit longer to deliver in the short term, but in the long term you gain a lot with TDD. However, it is quite hard to see the long term because it's not under our noses like the short term is. I have fought battles with stubborn bosses in my career, to be able to code using TDD. Sometimes it has been painful, but always well worth it, and I have never regretted it because, in the end, the quality of the result has always been appreciated.
- If you fail to understand the business requirements, this will reflect in the tests you write, and therefore it will reflect in the code too: This kind of problem is quite hard to spot until you do UAT, but one thing that you can do to reduce the likelihood of it happening is to pair with another developer. Pairing will inevitably require discussions about the business requirements, and this will help having a better idea about them before the tests are written.
- Badly written tests are hard to maintain: This is a fact. Tests with too many mocks or with extra assumptions or badly structured data will soon become a burden. Don't let this discourage you; just keep experimenting and change the way you write them until you find a way that doesn't require you a huge amount of work every time you touch your code.
I'm so passionate about TDD that when I interview for a job, I always ask if the company I'm about to join adopts it. If the answer is no, it's kind of a deal-breaker for me. I encourage you to check it out and use it. Use it until you feel something clicking in your mind. You won't regret it, I promise.
Exceptions
Even though I haven't formally introduced them to you, by now I expect you to at least have a vague idea of what an exception is. In the previous chapters, we've seen that when an iterator is exhausted, calling next
on it raises a StopIteration
exception. We've met IndexError
when we tried accessing a list at a position that was outside the valid range. We've also met AttributeError
when we tried accessing an attribute on an object that didn't have it, and KeyError
when we did the same with a key and a dictionary. We've also just met AssertionError
when running tests.
Now, the time has come for us to talk about exceptions.
Sometimes, even though an operation or a piece of code is correct, there are conditions in which something may go wrong. For example, if we're converting user input from string
to int
, the user could accidentally type a letter in place of a digit, making it impossible for us to convert that value into a number. When dividing numbers, we may not know in advance if we're attempting a division by zero. When opening a file, it could be missing or corrupted.
When an error is detected during execution, it is called an exception. Exceptions are not necessarily lethal; in fact, we've seen that StopIteration
is deeply integrated in Python generator and iterator mechanisms. Normally, though, if you don't take the necessary precautions, an exception will cause your application to break. Sometimes, this is the desired behavior but in other cases, we want to prevent and control problems such as these. For example, we may alert the user that the file they're trying to open is corrupted or that it is missing so that they can either fix it or provide another file, without the need for the application to die because of this issue. Let's see an example of a few exceptions:
exceptions/first.example.py
>>> gen = (n for n in range(2)) >>> next(gen) 0 >>> next(gen) 1 >>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration >>> print(undefined_var) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'undefined_var' is not defined >>> mylist = [1, 2, 3] >>> mylist[5] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range >>> mydict = {'a': 'A', 'b': 'B'} >>> mydict['c'] Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'c' >>> 1 / 0 Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero
As you can see, the Python shell is quite forgiving. We can see the Traceback
, so that we have information about the error, but the program doesn't die. This is a special behavior, a regular program or a script would normally die if nothing were done to handle exceptions.
To handle an exception, Python gives you the try
statement. What happens when you enter the try
clause is that Python will watch out for one or more different types of exceptions (according to how you instruct it), and if they are raised, it will allow you to react. The try
statement is comprised of the try
clause, which opens the statement; one or more except
clauses (all optional) that define what to do when an exception is caught; an else
clause (optional), which is executed when the try
clause is exited without any exception raised; and a finally
clause (optional), whose code is executed regardless of whatever happened in the other clauses. The finally
clause is typically used to clean up resources. Mind the order, it's important. Also, try
must be followed by at least one except
clause or a finally
clause. Let's see an example:
exceptions/try.syntax.py
def try_syntax(numerator, denominator): try: print('In the try block: {}/{}' .format(numerator, denominator)) result = numerator / denominator except ZeroDivisionError as zde: print(zde) else: print('The result is:', result) return result finally: print('Exiting') print(try_syntax(12, 4)) print(try_syntax(11, 0))
The preceding example defines a simple try_syntax
function. We perform the division of two numbers. We are prepared to catch a ZeroDivisionError
exception if we call the function with denominator = 0
. Initially, the code enters the try
block. If denominator
is not 0
, result
is calculated and the execution, after leaving the try
block, resumes in the else
block. We print result
and return it. Take a look at the output and you'll notice that just before returning result
, which is the exit point of the function, Python executes the finally
clause.
When denominator
is 0
, things change. We enter the except
block and print zde
. The else
block isn't executed because an exception was raised in the try
block. Before (implicitly) returning None
, we still execute the finally
block. Take a look at the output and see if it makes sense to you:
$ python exceptions/try.syntax.py In the try block: 12/4 The result is: 3.0 Exiting 3.0 In the try block: 11/0 division by zero Exiting None
When you execute a try
block, you may want to catch more than one exception. For example, when trying to decode a JSON object, you may incur into ValueError
for malformed JSON, or TypeError
if the type of the data you're feeding to json.loads()
is not a string. In this case, you may structure your code like this:
exceptions/json.example.py
import json json_data = '{}' try: data = json.loads(json_data) except (ValueError, TypeError) as e: print(type(e), e)
This code will catch both ValueError
and TypeError
. Try changing json_data = '{}'
to json_data = 2
or json_data = '{{'
, and you'll see the different output.
Note
JSON stands for JavaScript Object Notation and it's an open standard format that uses human-readable text to transmit data objects consisting of key/value pairs. It's an exchange format widely used when moving data across applications, especially when data needs to be treated in a language or platform-agnostic way.
If you want to handle multiple exceptions differently, you can just add more except
clauses, like this:
exceptions/multiple.except.py
try: # some code except Exception1: # react to Exception1 except (Exception2, Exception3): # react to Exception2 and Exception3 except Exception3: # react to Exception3 ...
Keep in mind that an exception is handled in the first block that defines that exception class or any of its bases. Therefore, when you stack multiple except
clauses like we've just done, make sure that you put specific exceptions at the top and generic ones at the bottom. In OOP terms, children on top, grandparents at the bottom. Moreover, remember that only one except
handler is executed when an exception is raised.
You can also write custom exceptions. In order to do that, you just have to inherit from any other exception class. Python built-in exceptions are too many to be listed here, so I have to point you towards the official documentation. One important thing to know is that every Python exception derives from BaseException
, but your custom exceptions should never inherit directly from that one. The reason for it is that handling such an exception will trap also system-exiting exceptions such as SystemExit
and KeyboardInterrupt
, which derive from BaseException
, and this could lead to severe issues. In case of disaster, you want to be able to Ctrl + C your way out of an application.
You can easily solve the problem by inheriting from Exception
, which inherits from BaseException
, but doesn't include any system-exiting exception in its children because they are siblings in the built-in exceptions hierarchy (see https://docs.python.org/3/library/exceptions.html#exception-hierarchy).
Programming with exceptions can be very tricky. You could inadvertently silence out errors, or trap exceptions that aren't meant to be handled. Play it safe by keeping in mind a few guidelines: always put in the try
clause only the code that may cause the exception(s) that you want to handle. When you write except
clauses, be as specific as you can, don't just resort to except Exception
because it's easy. Use tests to make sure your code handles edge cases in a way that requires the least possible amount of exception handling. Writing an except
statement without specifying any exception would catch any exception, therefore exposing your code to the same risks you incur when you derive your custom exceptions from BaseException
.
You will find information about exceptions almost everywhere on the web. Some coders use them abundantly, others sparingly (I belong to the latter category). Find your own way of dealing with them by taking examples from other people's source code. There's plenty of interesting projects whose sources are open, and you can find them on either GitHub (https://github.com) or Bitbucket (https://bitbucket.org/).
Before we talk about profiling, let me show you an unconventional use of exceptions, just to give you something to help you expand your views on them. They are not just simply errors.
exceptions/for.loop.py
n = 100 found = False for a in range(n): if found: break for b in range(n): if found: break for c in range(n): if 42 * a + 17 * b + c == 5096: found = True print(a, b, c) # 79 99 95
The preceding code is quite a common idiom if you deal with numbers. You have to iterate over a few nested ranges and look for a particular combination of a
, b
, and c
that satisfies a condition. In the example, condition is a trivial linear equation, but imagine something much cooler than that. What bugs me is having to check if the solution has been found at the beginning of each loop, in order to break out of them as fast as we can when it is. The break out logic interferes with the rest of the code and I don't like it, so I came up with a different solution for this. Take a look at it, and see if you can adapt it to other cases too.
exceptions/for.loop.py
class ExitLoopException(Exception): pass try: n = 100 for a in range(n): for b in range(n): for c in range(n): if 42 * a + 17 * b + c == 5096: raise ExitLoopException(a, b, c) except ExitLoopException as ele: print(ele) # (79, 99, 95)
Can you see how much more elegant it is? Now the breakout logic is entirely handled with a simple exception whose name even hints at its purpose. As soon as the result is found, we raise it, and immediately the control is given to the except clause which handles it. This is food for thought. This example indirectly shows you how to raise your own exceptions. Read up on the official documentation to dive into the beautiful details of this subject.
Profiling Python
There are a few different ways to profile a Python application. Profiling means having the application run while keeping track of several different parameters, like the number of times a function is called, the amount of time spent inside it, and so on. Profiling can help us find the bottlenecks in our application, so that we can improve only what is really slowing us down.
If you take a look at the profiling section in the standard library official documentation, you will see that there are a couple of different implementations of the same profiling interface: profile
and cProfile
.
cProfile
is recommended for most users, it's a C extension with reasonable overhead that makes it suitable for profiling long-running programsprofile
is a pure Python module whose interface is imitated bycProfile
, but which adds significant overhead to profiled programs
This interface does determinist profiling, which means that all function calls, function returns and exception events are monitored, and precise timings are made for the intervals between these events. Another approach, called statistical profiling, randomly samples the effective instruction pointer, and deduces where time is being spent.
The latter usually involves less overhead, but provides only approximate results. Moreover, because of the way the Python interpreter runs the code, deterministic profiling doesn't add that as much overhead as one would think, so I'll show you a simple example using cProfile
from the command line.
We're going to calculate Pythagorean triples (I know, you've missed them...) using the following code:
profiling/triples.py
def calc_triples(mx): triples = [] for a in range(1, mx + 1): for b in range(a, mx + 1): hypotenuse = calc_hypotenuse(a, b) if is_int(hypotenuse): triples.append((a, b, int(hypotenuse))) return triples def calc_hypotenuse(a, b): return (a**2 + b**2) ** .5 def is_int(n): # n is expected to be a float return n.is_integer() triples = calc_triples(1000)
The script is extremely simple; we iterate over the interval [1, mx] with a
and b
(avoiding repetition of pairs by setting b >= a
) and we check if they belong to a right triangle. We use calc_hypotenuse
to get hypotenuse
for a
and b
, and then, with is_int
, we check if it is an integer, which means (a, b, c) is a Pythagorean triple. When we profile this script, we get information in tabular form. The columns are ncalls
, tottime
, percall
, cumtime
, percall
, and filename:lineno(function)
. They represent the amount of calls we made to a function, how much time we spent in it, and so on. I'll trim a couple of columns to save space, so if you run the profiling yourself, don't worry if you get a different result.
$ python -m cProfile profiling/triples.py 1502538 function calls in 0.750 seconds Ordered by: standard name ncalls tottime percall filename:lineno(function) 500500 0.469 0.000 triples.py:14(calc_hypotenuse) 500500 0.087 0.000 triples.py:18(is_int) 1 0.000 0.000 triples.py:4(<module>) 1 0.163 0.163 triples.py:4(calc_triples) 1 0.000 0.000 {built-in method exec} 1034 0.000 0.000 {method 'append' of 'list' objects} 1 0.000 0.000 {method 'disable' of '_lsprof.Profil... 500500 0.032 0.000 {method 'is_integer' of 'float' objects}
Even with this limited amount of data, we can still infer some useful information about this code. Firstly, we can see that the time complexity of the algorithm we have chosen grows with the square of the input size. The amount of times we get inside the inner loop body is exactly mx (mx + 1) / 2. We run the script with mx = 1000
, which means we get 500500
times inside the inner for
loop. Three main things happen inside that loop, we call calc_hypotenuse
, we call is_int
and, if the condition is met, we append to the triples
list.
Taking a look at the profiling report, we notice that the algorithm has spent 0.469
seconds inside calc_hypotenuse
, which is way more than the 0.087
seconds spent inside is_int
, given that they were called the same number of times, so let's see if we can boost calc_hypotenuse
a little.
As it turns out, we can. As I mentioned earlier on in the book, the power operator **
is quite expensive, and in calc_hypotenuse,
we're using it three times. Fortunately, we can easily transform two of those into simple multiplications, like this:
profiling/triples.py
def calc_hypotenuse(a, b): return (a*a + b*b) ** .5
This simple change should improve things. If we run the profiling again, we see that now the 0.469
is now down to 0.177
. Not bad! This means now we're spending only about 37% of the time inside calc_hypotenuse
as we were before.
Let's see if we can improve is_int
as well, by changing it like this:
profiling/triples.py
def is_int(n): return n == int(n)
This implementation is different and the advantage is that it also works when n
is an integer. Alas, when we run the profiling against it, we see that the time taken inside the is_int
function has gone up to 0.141
seconds. This means that it has roughly doubled, compared to what it was before. In this case, we need to revert to the previous implementation.
This example was trivial, of course, but enough to show you how one could profile an application. Having the amount of calls that are performed against a function helps us understand better the time complexity of our algorithms. For example, you wouldn't believe how many coders fail to see that those two for
loops run proportionally to the square of the input size.
One thing to mention: depending on what system you're using, results may be different. Therefore, it's quite important to be able to profile software on a system that is as close as possible to the one the software is deployed on, if not actually on that one.
When to profile?
Profiling is super cool, but we need to know when it is appropriate to do it, and in what measure we need to address the results we get from it.
Donald Knuth once said that premature optimization is the root of all evil and, although I wouldn't have put it down so drastically, I do agree with him. After all, who am I to disagree with the man that gave us The Art of Computer Programming, TeX, and some of the coolest algorithms I have ever studied when I was a university student?
So, first and foremost: correctness. You want you code to deliver the result correctly, therefore write tests, find edge cases, and stress your code in every way you think makes sense. Don't be protective, don't put things in the back of your brain for later because you think they're not likely to happen. Be thorough.
Secondly, take care of coding best practices. Remember readability, extensibility, loose coupling, modularity, and design. Apply OOP principles: encapsulation, abstraction, single responsibility, open/closed, and so on. Read up on these concepts. They will open horizons for you, and they will expand the way you think about code.
Thirdly, refactor like a beast! The Boy Scouts Rule says to Always leave the campground cleaner than you found it. Apply this rule to your code.
And, finally, when all of the above has been taken care of, then and only then, you take care of profiling.
Run your profiler and identify bottlenecks. When you have an idea of the bottlenecks you need to address, start with the worst one first. Sometimes, fixing a bottleneck causes a ripple effect that will expand and change the way the rest of the code works. Sometimes this is only a little, sometimes a bit more, according to how your code was designed and implemented. Therefore, start with the biggest issue first.
One of the reasons Python is so popular is that it is possible to implement it in many different ways. So, if you find yourself having troubles boosting up some part of your code using sheer Python, nothing prevents you from rolling up your sleeves, buying a couple of hundred liters of coffee, and rewriting the slow piece of code in C. Guaranteed to be fun!
When to profile?
Profiling is super cool, but we need to know when it is appropriate to do it, and in what measure we need to address the results we get from it.
Donald Knuth once said that premature optimization is the root of all evil and, although I wouldn't have put it down so drastically, I do agree with him. After all, who am I to disagree with the man that gave us The Art of Computer Programming, TeX, and some of the coolest algorithms I have ever studied when I was a university student?
So, first and foremost: correctness. You want you code to deliver the result correctly, therefore write tests, find edge cases, and stress your code in every way you think makes sense. Don't be protective, don't put things in the back of your brain for later because you think they're not likely to happen. Be thorough.
Secondly, take care of coding best practices. Remember readability, extensibility, loose coupling, modularity, and design. Apply OOP principles: encapsulation, abstraction, single responsibility, open/closed, and so on. Read up on these concepts. They will open horizons for you, and they will expand the way you think about code.
Thirdly, refactor like a beast! The Boy Scouts Rule says to Always leave the campground cleaner than you found it. Apply this rule to your code.
And, finally, when all of the above has been taken care of, then and only then, you take care of profiling.
Run your profiler and identify bottlenecks. When you have an idea of the bottlenecks you need to address, start with the worst one first. Sometimes, fixing a bottleneck causes a ripple effect that will expand and change the way the rest of the code works. Sometimes this is only a little, sometimes a bit more, according to how your code was designed and implemented. Therefore, start with the biggest issue first.
One of the reasons Python is so popular is that it is possible to implement it in many different ways. So, if you find yourself having troubles boosting up some part of your code using sheer Python, nothing prevents you from rolling up your sleeves, buying a couple of hundred liters of coffee, and rewriting the slow piece of code in C. Guaranteed to be fun!
Summary
In this chapter, we explored the world of testing, exceptions, and profiling.
I tried to give you a fairly comprehensive overview of testing, especially unit testing, which is the kind of testing that a developer mostly does. I hope I have succeeded in channeling the message that testing is not something that is perfectly defined and that you can learn from a book. You need to experiment with it a lot before you get comfortable. Of all the efforts a coder must make in terms of study and experimentation, I'd say testing is one of those that are most worth it.
We've briefly seen how we can prevent our program from dying because of errors, called exceptions, that happen at runtime. And, to steer away from the usual ground, I have given you an example of a somewhat unconventional use of exceptions to break out of nested for
loops. That's not the only case, and I'm sure you'll discover others as you grow as a coder.
In the end, we very briefly touched base on profiling, with a simple example and a few guidelines. I wanted to talk about profiling for the sake of completeness, so at least you can play around with it.
We're now about to enter Chapter 8, The Edges – GUIs and Scripts, where we're going to get our hands dirty with scripts and GUIs and, hopefully, come up with something interesting.
Note
I am aware that I gave you a lot of pointers in this chapter, with no links or directions. I'm afraid this is by choice. As a coder, there won't be a single day at work when you won't have to look something up in a documentation page, in a manual, on a website, and so on. I think it's vital for a coder to be able to search effectively for the information they need, so I hope you'll forgive me for this extra training. After all, it's all for your benefit.