Two different radians() functions
MP 48: Why does the order of import statements matter?
A reader recently wrote with a question that has a fairly straightforward answer. But taking time to dig a little deeper into their question touches on a number of different things that are helpful to know as a Python programmer. Let’s look at their question, and follow a few rabbit holes.
The question
Here’s the question, slightly paraphrased:
I ran the following code in a terminal session:
>>> from math import *
>>> from turtle import *
>>> radians(30)
and then ran it with the import
statements reversed:
>>> from turtle import *
>>> from math import *
>>> radians(30)
The results are different. Do you know why?
I knew the main reason there was a difference, but I didn’t know exactly what the difference would be. Let’s start answering this question, and see where it leads.
Degrees vs. Radians
There’s an important mathematical concept at the core of this question that many people probably aren’t familiar with. Degrees and radians are both used to measure angles. These units are defined by different approaches to the challenge of dividing a circle.
In a degree-based system, a circle is divided into 360 parts; one degree is 1/360th of a full circle. In a radian-based system, all divisions of the circle are based on π. There are 2π radians in a full circle.
How and why we divide circles in different ways is an incredibly rich and interesting rabbit hole! The important thing to know if this is new to you is that you can convert between the two systems using the fact that 360 degrees is equal to 2π radians.1
Running the code
When someone asks me a question like this, I always run their actual code before digging too much. Running the code either validates the question itself, or points to a simpler issue such as a typo, a file that’s being run with unsaved changes, or an issue with their environment.
Here’s the output of the first example:
>>> from math import *
>>> from turtle import *
>>> radians(30)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: radians() takes 0 positional arguments but 1 was given
This version generates a TypeError
.
We’re dealing with the order of import
statements, so it’s a good idea to run the second example in a new terminal session. I closed out the previous terminal, opened a new one, and ran the example with the import
statements reversed:
>>> from turtle import *
>>> from math import *
>>> radians(30)
0.5235987755982988
I can see why this might be confusing. It seems reasonable to think that import
statements are just some setup work that we do, and then we can write code that should work consistently from that point forward. But depending on how you structure your import
statements, that’s not always true.
Namespace collisions: The problem with import *
If you haven’t heard this advice before, I’ll state it quite clearly:
Don’t use the from package_name import *
syntax.
When you run the statement from math import *
, you import every function and class defined in the math
module into your program. You don’t see all that code, but it’s loaded at runtime. That’s why this code works:
>>> from math import *
>>> radians(30)
0.5235987755982988
The problem with this approach is that the radians()
function from the math
module will conflict with any other function named radians()
. In fact, it will conflict with any other object named radians
, including a simple variable.
Consider this example:
>>> from math import *
>>> radians(30)
0.5235987755982988
>>> radians = 25
>>> radians(30)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
Here we call the radians()
function, but then we define a variable with the name radians
. When we try to call the radians()
function again, we get a TypeError
. This happens because the name radians
is no longer associated with a function; it’s now associated with an integer value.
A similar problem happens if we first define the variable, and then import everything from the math
module:
>>> radians = 25
>>> radians
25
>>> from math import *
>>> radians
<built-in function radians>
There’s no traceback here, but there is a problem. We defined a variable, radians
, that we probably care about. When we import the math
module, we lose the value that we originally set.
This kind of problem is called a namespace collision.
A namespace is a collection of names. There’s a namespace for your program, consisting of all the names you’ve defined. There’s also a namespace for the math
module, consisting of all the names that are used in that module. When we use import *
, we cause those two namespaces to clash.2
We can talk about the order of imports to clarify things, but it’s not worth going down that road any further. Namespace collisions will almost always cause problems. There are simple ways to avoid them, and you should absolutely do so.
Better approaches
There are a number of approaches to importing that will avoid namespace collisions:
Importing an entire module
First, you can import an entire module, instead of importing all the functions and classes from the module:
>>> import math
>>> math.radians(30)
0.5235987755982988
This approach prevents collisions because it puts all of the names defined in math
into their own space. Whenever you want to use something defined in the math
module, you have to prefix it with math.
This prefix makes it clear whenever you’re using something from the math
module.
Also, and just as importantly, you can use any variable name you want without worrying that you might be conflicting with a name already in use in the math
module. It would be a serious problem if you couldn’t manage namespaces like this, because it would be a lot of work to check for naming conflicts between your own code, all the libraries you import directly, and all the dependencies that those libraries each import.
With this approach, you can use functions of the same names from different libraries:
>>> import math
>>> import turtle
>>> math.radians(30)
0.5235987755982988
>>> turtle.radians()
>>>
There’s no conflict here, because it’s clear which radians()
function we’re calling on each line. We’ll say more about why there’s no argument to turtle.radians()
in a moment. For now the important part is that it doesn’t generate an error.
You can also give the module an alias if you don’t want to repeat the module’s full name throughout your program:
>>> import turtle as t
>>> t.radians()
>>>
This is just as effective at managing namespace collisions as using the full module name. If you’re using an alias, it’s a good idea to find out if there’s a common convention for the library you’re using. For example with pandas
people commonly use the alias pd
, and people usually use np
for NumPy.
Importing a single function
If you don’t want to import the entire module, you can import the specific functions or classes you want to work with:
>>> from math import radians
This is usually fine, as you can reasonably expect to avoid writing your own function that overwrites this one name. The main benefit to this approach is that it’s explicit about what the math module is being used for in this project.
People are quite unlikely to write something like this:
>>> from math import radians
>>> from turtle import radians
It’s reasonably clear from looking at this code that we’re importing two different things with the same name, which will only cause confusion.
Running help()
against the imported functions
Let’s take a step back, and imagine we hadn’t realized there was a namespace collision. Python’s help()
function can be useful here.
>>> from math import *
>>> from turtle import *
>>> help(radians)
Here’s the help message that appears:
Help on function radians in module turtle:
radians()
Set the angle measurement units to radians.
No arguments.
Example:
>>> heading()
90
>>> radians()
>>> heading()
1.5707963267948966
There are a couple things to note here:
The first line of output shows that
radians
is currently pointing to theradians()
function from theturtle
module.Also, it clarifies what this function does: it sets the unit of angle measurement to radians for all subsequent actions.3
Let’s see what happens when we call help(radians)
with the import
statements reversed:
>>> from turtle import *
>>> from math import *
>>> help(radians)
Here’s the help()
output:
Help on built-in function radians in module math:
radians(x, /)
Convert angle x from degrees to radians.
Again, this tells us two import things:
- In this snippet,
radians
points to a function calledradians()
from themath
module. - This function converts an angle from degrees to radians.
Calling help()
in these situations doesn’t teach you about Python’s importing rules, but it clarifies why you’re seeing the different behavior each time. Looking at documentation like this can lead you to a better understanding of how to manage imports and names in a project.
Conclusions
When you have a question about the behavior of your code in Python, there are a number of things you can try yourself to better understand what’s happening. Don’t be afraid to drop into a terminal session and run help()
on the object or function you’re working with.
Also, continue to be curious about what’s happening under the hood when your code is executed. The more you understand what Python does for you in the background, the better your mental model will be of what’s happening as your code runs. That understanding will become increasingly helpful as you troubleshoot each new issue that arises in your projects.
There’s an old saying that’s always worth repeating:
Naming things is hard.
When people first hear this, they tend to think it’s hard to come up with good descriptive variable names, and that’s it. But the difficulties that come up around names in programming are quite varied, and not at all obvious. Avoid some common naming issues in Python by using an import
syntax that gives you the kind of namespaces most appropriate for your project.
Resources
You can find the code files from this post in the mostly_python GitHub repository.
Do you have a question you’d like to see answered? You can reply to any post, and I’ll do my best to either answer your question, or schedule a post that dives into the bigger issues your question brings up. Questions that get at how Python behaves are much more likely to be answered than narrow questions about a specific project you’re working on. You can see more posts about reader questions here.
If you’re interested in the rabbit hole, here’s a couple things to help you jump in. There are quite a few ways to divide a circle:
HH:MM:SS - I believe this is most often used in astronomy. There are 24 hours in a full circle; there are 60 minutes in an hour, and there are 60 seconds in a minute. In this system, an angle of one minute is 1/(24*60), or 1/1440th of a circle. An angle of one second is 1/(24*60*60), or 1/86,400th of a circle.
DD:MM:SS - This system, degrees-minutes-seconds is used in some navigation settings. In this system a minute is 1/60th of a degree. This works out to 1/(360*60), or 1/21,600th of a circle. This is much different than a one-minute angle in the HH:MM:SS system!
DD.DDD - This is a simple system where every angle is simply in degrees, with as many decimal places as needed.
A gradian is 1/400th of a circle. It was developed as part of the metric system, so that a right angle is 100 gradians.
I spent many years volunteering on a mountain rescue team, and we had to be really careful about communicating positions using latitude and longitude. Locations could be measured by any number of devices: handheld GPS units, boat-, plane- and helicopter-based GPS units, cell phones, maps, etc.
We couldn’t assume that any two set of lat-lon coordinates were generated using the same units, and lot of communication centered around asking people what device they were using, and verifying the default units on that device. This was a bigger issue when consumer handheld GPS units first became available, and the default conventions on different devices didn’t always match what was used in the professional rescue community.
If you’re curious to read more about different angle units, here’s a good article about why there are 360 degrees in a circle. ↩
There are other namespaces as well. For example Python’s built-in functions and keywords are part of your program’s namespace. That’s why you shouldn’t use names that Python is already using, like
print
:>>> print("Hello!") Hello! >>> print = 5 >>> print("Hello!") Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'int' object is not callable
Here we’ve created a namespace collision with the built-in
print()
function. We’ve set the nameprint
to point to the integer value5
, instead of theprint()
function. Now we’ve lost the ability to callprint()
! ↩I don’t think the documentation shows a good example for
radians()
, because the number 1.570796 doesn’t mean much to most people. That number is actually π/2 in radians.Some people think more clearly in radians, and instead of making a “90-degree turn”, they want to specify an action such as “turn π/2 radians”. Here’s a quick example of what this might look like:
>>> import turtle as t >>> from math import pi as PI >>> t.radians() >>> t.left(PI/2)
This effectively says, “turn the turtle counterclockwise π/2 radians”, which is equivalent to turning left 90 degrees. ↩