Simplify Complex Numbers With Python

Simplify Complex Numbers With Python

by Bartosz Zaczyński Jun 21, 2021 intermediate python

Most general-purpose programming languages have either no support or limited support for complex numbers. Your typical options are learning some specialized tool like MATLAB or finding a third-party library. Python is a rare exception because it comes with complex numbers built in.

Despite the name, complex numbers aren’t complicated! They’re convenient in tackling practical problems that you’ll get a taste of in this tutorial. You’ll explore vector graphics and sound frequency analysis, but complex numbers can also help in drawing fractals, for example.

In this tutorial, you’ll learn how to:

  • Define complex numbers with literals in Python
  • Represent complex numbers in rectangular and polar coordinates
  • Use complex numbers in arithmetic expressions
  • Take advantage of the built-in cmath module
  • Translate mathematical formulas directly to Python code

If you need a quick refresher or a gentle introduction to the theory of complex numbers, then you can watch Khan Academy’s video series. To download the sample code used throughout this tutorial, click the link below:

Creating Complex Numbers in Python

Creating and manipulating complex numbers in Python isn’t much different from other built-in data types, particularly numeric types. It’s possible because the language treats them as first-class citizens. This means you can express mathematical formulas that involve complex numbers with little overhead.

Python lets you use complex numbers in arithmetic expressions and call functions on them just like you would with other numbers in Python. It leads to elegant syntax that reads almost like a math textbook.

Complex Number Literal

The quickest way to define a complex number in Python is by typing its literal directly in the source code:

>>>
>>> z = 3 + 2j

Although this looks like an algebraic formula, the expression to the right of the equals sign is already a fixed value that needs no further evaluation. When you check its type, you’ll confirm that it’s indeed a complex number:

>>>
>>> type(z)
<class 'complex'>

How is that different from adding two numbers with the plus operator? A clear giveaway is the letter j glued to the second number, which completely changes the meaning of the expression. If you removed the letter, you’d get a familiar integer result instead:

>>>
>>> z = 3 + 2

>>> type(z)
<class 'int'>

By the way, you can use floating-point numbers to create complex numbers, too:

>>>
>>> z = 3.14 + 2.71j
>>> type(z)
<class 'complex'>

Complex number literals in Python mimic the mathematical notation, which is also known as the standard form, the algebraic form, or sometimes the canonical form, of a complex number. In Python, you can use either lowercase j or uppercase J in those literals.

If you learned about complex numbers in math class, you might have seen them expressed using an i instead of a j. If you’re curious about why Python uses j instead of i, then you can expand the collapsible section below to learn more.

The traditional notation for complex numbers uses the letter i instead of j since it stands for the imaginary unit. You might feel a slight discomfort with Python’s convention if you have a mathematical background. However, there are a few reasons that can justify Python’s controversial choice:

  • It’s a convention already adopted by engineers to avoid name collisions with electric current, which is denoted with the letter i.
  • In computing, the letter i is often used for the indexing variable in loops.
  • The letter i can be easily confused with l or 1 in source code.

This was brought up on Python’s bug tracker over a decade ago, and Python’s creator, Guido van Rossum himself, closed the issue with this comment:

This will not be fixed. For one thing, the letter ‘i’ or upper case ‘I’ look too much like digits. The way numbers are parsed either by the language parser (in source code) or by the built-in functions (int, float, complex) should not be localizable or configurable in any way; that’s asking for huge disappointments down the road. If you want to parse complex numbers using ‘i’ instead of ‘j’, you have plenty of solutions available already. (Source)

So there you have it. Unless you want to start using MATLAB, you’ll have to live with using j to denote your complex numbers.

The algebraic form of a complex number follows the standard rules of algebra, which is convenient in performing arithmetic. For example, addition has a commutative property, which lets you swap the order of the two parts of a complex number literal without changing its value:

>>>
>>> 3 + 2j == 2j + 3
True

Similarly, you can substitute addition for subtraction in a complex number literal because the minus sign is just a shorthand notation for an equivalent form:

>>>
>>> 3 - 2j == 3 + (-2j)
True

Does a complex number literal in Python always have to comprise two numbers? Can it have more? Are they ordered? To answer these questions, let’s run some experiments. Unsurprisingly, if you specify only one number, without the letter j, then you’ll end up with a regular integer or a floating-point number:

>>>
>>> z = 3.14
>>> type(z)
<class 'float'>

On the other hand, appending the letter j to a numeric literal will immediately turn it into a complex number:

>>>
>>> z = 3.14j
>>> type(z)
<class 'complex'>

Strictly speaking, from a mathematical standpoint, you’ve just created a pure imaginary number, but Python can’t represent it as a stand-alone data type. Therefore, without the other part, it’s just a complex number .

How about the opposite? To create a complex number without the imaginary part, you can take advantage of zero and add or subtract it like so:

>>>
>>> z = 3.14 + 0j
>>> type(z)
<class 'complex'>

In fact, both parts of the complex number are always there. When you don’t see one, it means that it has a value of zero. Let’s check what happens when you try stuffing more terms into the sum than before:

>>>
>>> 2 + 3j + 4 + 5j
(6+8j)

This time, your expression is no longer a literal because Python evaluated it into a complex number comprising only two parts. Remember that the basic rules of algebra carry over to complex numbers, so if you group similar terms and apply component-wise addition, then you’ll end up with 6 + 8j.

Notice how Python displays complex numbers by default. Their textual representation contains an enclosing pair of parentheses, a lowercase letter j, and no whitespace. Additionally, the imaginary part comes second.

Complex numbers that also happen to be pure imaginary numbers show up without parentheses and only reveal their imaginary part:

>>>
>>> 3 + 0j
(3+0j)
>>> 0 + 3j
3j

This helps differentiate imaginary numbers from most complex numbers made up of real and imaginary parts.

complex() Factory Function

Python has a built-in function, complex(), that you can use as an alternative to the complex number literal:

>>>
>>> z = complex(3, 2)

In this form, it resembles a tuple or an ordered pair of ordinary numbers. The analogy isn’t that far-fetched. Complex numbers have a geometric interpretation in the Cartesian coordinate system that you’ll explore in a bit. You can think of complex numbers as two-dimensional.

The complex number factory function accepts two numeric parameters. The first one represents the real part, while the second one represents the imaginary part denoted with the letter j in the literal you saw earlier:

>>>
>>> complex(3, 2) == 3 + 2j
True

Both parameters are optional, with default values of zero, which makes it less clunky to define complex numbers without the imaginary part or both the real and imaginary parts:

>>>
>>> complex(3) == 3 + 0j
True
>>> complex() == 0 + 0j
True

The single-argument version can be useful in type casting. For example, you can pass a nonnumeric value like a string literal to obtain a corresponding complex object. Note that the string can’t contain any whitespace, though:

>>>
>>> complex("3+2j")
(3+2j)

>>> complex("3 + 2j")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: complex() arg is a malformed string

Later, you’ll find out how to make your classes compatible with this type casting mechanism. Interestingly, when you pass a complex number to complex(), you’ll get the same instance back:

>>>
>>> z = complex(3, 2)
>>> z is complex(z)
True

That’s consistent with how other types of numbers in Python work because they’re all immutable. To make a distinct copy of a complex number, you must call the function with both arguments again or declare another variable with the complex number literal:

>>>
>>> z = complex(3, 2)
>>> z is complex(3, 2)
False

When you provide two arguments to the function, they must always be numbers, such as int, float, or complex. Otherwise, you’ll get a runtime error. Technically speaking, bool is a subclass of int, so it’ll work too:

>>>
>>> complex(False, True)  # Booleans, same as complex(0, 1)
1j

>>> complex(3, 2)  # Integers
(3+2j)

>>> complex(3.14, 2.71)  # Floating-point numbers
(3.14+2.71j)

>>> complex("3", "2")  # Strings
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: complex() can't take second arg if first is a string

Things get seemingly more bizarre when you supply the complex() factory function with complex numbers as arguments. If you provide only the first argument, though, it’ll behave like a proxy as before:

>>>
>>> complex(complex(3, 2))
(3+2j)

However, when two arguments are present and at least one of them is a complex number, you’ll get results that may be difficult to explain at first sight:

>>>
>>> complex(1, complex(3, 2))
(-1+3j)

>>> complex(complex(3, 2), 1)
(3+3j)

>>> complex(complex(3, 2), complex(3, 2))
(1+5j)

To get the answers, let’s take a peek at the factory function’s docstring or the online documentation, which explain what’s going on under the hood when you call complex(real, imag):

Return a complex number with the value real + imag*1j or convert a string or number to a complex number. (Source)

In this explanation, real and imag are names of the function arguments. The second argument gets multiplied by the imaginary unit j, and the result is added to the first argument. Don’t worry if it still doesn’t make any sense. You can come back to this part when you’ve read about complex numbers arithmetic. The rules that you’ll learn about will make this straightforward.

When would you want to use the complex() factory function over the literal? It depends, but calling the function may be more convenient when you’re dealing with dynamically generated data, for example.

Getting to Know Python Complex Numbers

In mathematics, complex numbers are a superset of real numbers, which means that every real number is also a complex number whose imaginary part is equal to zero. Python models this relationship through a concept called the numeric tower, described in PEP 3141:

>>>
>>> import numbers
>>> issubclass(numbers.Real, numbers.Complex)
True

The built-in numbers module defines a hierarchy of numeric types through abstract classes that can be used for type checking and classifying numbers. For example, to determine if a value belongs to a specific set of numbers, you can call isinstance() on it:

>>>
>>> isinstance(3.14, numbers.Complex)
True
>>> isinstance(3.14, numbers.Integral)
False

The floating-point value 3.14 is a real number that also happens to be a complex number but not an integral one. Note that you can’t use built-in types directly in such a test:

>>>
>>> isinstance(3.14, complex)
False

The difference between complex and numbers.Complex is that they belong to separate branches in the numeric type hierarchy tree, and the latter is an abstract base class without any implementation:

Type Hierarchy for Numbers in Python
Type hierarchy for numbers in Python

Abstract base classes, which are denoted in red on the diagram above, can bypass the regular inheritance check mechanism by registering unrelated classes as their virtual subclasses. That’s why a floating-point value in the example appears to be an instance of numbers.Complex but not complex.

Accessing Real and Imaginary Parts

To get the real and imaginary parts of a complex number in Python, you can reach for the corresponding .real and .imag attributes:

>>>
>>> z = 3 + 2j
>>> z.real
3.0
>>> z.imag
2.0

Both properties are read-only because complex numbers are immutable, so trying to assign a new value to either of them will fail:

>>>
>>> z.real = 3.14
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: readonly attribute

Since every number in Python is a more specific type of a complex number, attributes and methods defined in numbers.Complex are also available in all numeric types, including int and float:

>>>
>>> x = 42
>>> x.real
42
>>> x.imag
0

The imaginary part of such numbers is always zero.

Calculating the Conjugate of a Complex Number

Python complex numbers have only three public members. Apart from the .real and .imag properties, they expose the .conjugate() method, which flips the sign of the imaginary part:

>>>
>>> z = 3 + 2j
>>> z.conjugate()
(3-2j)

For numbers whose imaginary part equals zero, it won’t have any effect:

>>>
>>> x = 3.14
>>> x.conjugate()
3.14

This operation is its own inverse, so calling it twice will get you the original number you started with:

>>>
>>> z.conjugate().conjugate() == z
True

While it may seem of little value, the complex conjugate has a few useful arithmetic properties that can help calculate the division of two complex numbers with pen and paper, among many other things.

Complex Numbers Arithmetic

Since complex is a native data type in Python, you can plug complex numbers into arithmetic expressions and call many of the built-in functions on them. More advanced functions for complex numbers are defined in the cmath module, which is part of the standard library. You’ll get an introduction to it in a later part of this tutorial.

For now, remembering a single rule will let you apply your primary school knowledge of arithmetic to calculate basic operations involving complex numbers. The rule to remember is the definition of the imaginary unit, which satisfies the following equation:

The Imaginary Unit Definition

It doesn’t look right when you think of j as a real number, but don’t panic. If you ignore it for a moment and substitute every occurrence of j2 with -1 as if it were a constant, then you’ll be set. Let’s see how that works.

Addition

The sum of two or more complex numbers is equivalent to adding their real and imaginary parts component-wise:

>>>
>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 + z2
(6+8j)

Earlier, you found out that algebraic expressions composed of real and imaginary numbers follow the standard rules of algebra. When you write it down algebraically, you’ll be able to apply the distributive property and simplify the formula by factoring out and grouping common terms:

Adding Complex Numbers

Python automatically promotes operands to the complex data type when you add values of mixed numeric types:

>>>
>>> z = 2 + 3j
>>> z + 7  # Add complex to integer
(9+3j)

That is similar to the implicit conversion from int to float, which you might be more familiar with.

Subtraction

Subtracting complex numbers is analogous to adding them, which means you can also apply it element-wise:

>>>
>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 - z2
(-2-2j)

Unlike the sum, however, the order of operands is significant and yields different results just like with real numbers:

>>>
>>> z1 + z2 == z2 + z1
True
>>> z1 - z2 == z2 - z1
False

You can also use the unary minus operator (-) to make the negative of a complex number:

>>>
>>> z = 3 + 2j
>>> -z
(-3-2j)

This inverts both the real and the imaginary parts of the complex number.

Multiplication

The product of two or more complex numbers gets more interesting:

>>>
>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 * z2
(-7+22j)

How on Earth did you end up with a negative number out of only positive ones? To answer this question, you have to recall the definition of the imaginary unit and rewrite the expression in terms of real and imaginary parts:

Multiplying Complex Numbers

The key observation to make is that j times j gives j2, which can be replaced with -1. This inverts the sign of one of the summands, while the rest of the rules stay exactly the same as before.

Division

Dividing complex numbers can look intimidating at first encounter:

>>>
>>> z1 = 2 + 3j
>>> z2 = 4 + 5j
>>> z1 / z2
(0.5609756097560976+0.0487804878048781j)

Believe it or not, you’re able to get the same result using nothing more than a pen and paper! (Okay, a calculator might save you some headaches down the line.) When both numbers are expressed in their standard forms, the trick is to multiply the numerator and the denominator by the conjugate of the latter:

Dividing Complex Numbers

The denominator becomes a squared modulus of the divisor. You’ll learn more about the modulus of complex numbers later. When you continue deriving the formula, this is what you’ll get:

Dividing Complex Numbers

Note that complex numbers don’t support floor division, also known as integer division:

>>>
>>> z1 // z2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't take floor of complex number.

>>> z1 // 3.14
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't take floor of complex number.

This used to work in Python 2.x but was later removed to avoid ambiguity.

Exponentiation

You can raise complex numbers to a power using the binary exponentiation operator (**) or the built-in pow() but not the one defined in the math module, which only supports floating-point values:

>>>
>>> z = 3 + 2j

>>> z**2
(5+12j)

>>> pow(z, 2)
(5+12j)

>>> import math
>>> math.pow(z, 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't convert complex to float

Both the base and the exponent can be of any numeric types, including integer, floating-point, imaginary, or complex:

>>>
>>> 2**z
(1.4676557979464138+7.86422192328995j)

>>> z**2
(5+12j)

>>> z**0.5
(1.8173540210239707+0.5502505227003375j)

>>> z**3j
(-0.13041489185767086-0.11115341486478239j)

>>> z**z
(-5.409738793917679-13.410442370412747j)

Manual exponentiation of complex numbers becomes very difficult when they’re expressed in their standard form. It’s much more convenient to rewrite the number in the trigonometric form and calculate the power using some basic trigonometry. If you’re interested in the math involved, check out De Moivre’s formula, which lets you do that.

Using Python Complex Numbers as 2D Vectors

You can visualize complex numbers as points or vectors on a Euclidean plane in the Cartesian or rectangular coordinate system:

Complex Plane

The X-axis on the complex plane, also known as the Gauss plane or Argand diagram, represents the real part of a complex number, while the Y-axis represents its imaginary part.

This fact leads to one of the coolest features of the complex data type in Python, which embodies a rudimentary implementation of a two-dimensional vector for free. While not all operations work the same way in both of them, vectors and complex numbers share many similarities.

Getting the Coordinates

The Bermuda Triangle is a legendary region known for its paranormal phenomena that spans across the southern tip of Florida, Puerto Rico, and the tiny island of Bermuda. Its vertices are approximately designated by the three major cities whose geographical coordinates are the following:

  1. Miami: 25° 45’ 42.054” N, 80° 11’ 30.438” W
  2. San Juan: 18° 27’ 58.8” N, 66° 6’ 20.598” W
  3. Hamilton: 32° 17’ 41.64” N, 64° 46’ 58.908” W

After converting these coordinates to decimal degrees, you’ll end up with two floating-point numbers for each city. You can use the complex data type to store ordered pairs of numbers. Since the latitude is the vertical coordinate and the longitude is the horizontal one, it might be more convenient to switch them around to follow the traditional order of the Cartesian coordinates:

miami_fl = complex(-80.191788, 25.761681)
san_juan = complex(-66.105721, 18.466333)
hamilton = complex(-64.78303, 32.2949)

Negative longitude values represent the western hemisphere, while positive latitude values represent the northern hemisphere.

Bear in mind that these are spherical coordinates. To correctly project them onto a flat plane, you’d need to account for the curvature of Earth. One of the first map projections used in cartography was the Mercator projection, which helped sailors navigate their ships. But let’s ignore all that and assume that values are already expressed in the rectangular coordinate system.

When you plot the numbers on a complex plane, you’ll get a rough depiction of the Bermuda Triangle:

The Bermuda Triangle

In the companion materials, you’ll find an interactive Jupyter Notebook that plots the Bermuda Triangle using the Matplotlib library. To download the source code and materials for this tutorial, click the link below:

If you don’t like calling the complex() factory function, you can create a type alias with a better-suited name or use the literal form of a complex number to save a few keystrokes:

CityCoordinates = complex
miami_fl = CityCoordinates(-80.191788, 25.761681)
miami_fl = -80.191788 + 25.761681j

If you needed to pack more attributes on a city, you could use a named tuple or a data class or create a custom class.

Calculating the Magnitude

The magnitude, also known as the modulus or radius of a complex number, is the length of the vector that depicts it on a complex plane:

Complex Number as a Vector

You can calculate it from the Pythagorean theorem by taking the square root of the sum of the squared real part and the squared imaginary part:

Calculating the Complex Magnitude

You would think that Python would let you calculate the length of such a vector with the built-in len(), but that’s not the case. To get the magnitude of a complex number, you have to call another global function named abs(), which is typically used for calculating the absolute value of a number:

>>>
>>> len(3 + 2j)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'complex' has no len()

>>> abs(3 + 2j)
3.605551275463989

This function removes the sign from integers that you pass in, but for complex numbers, it returns the magnitude or vector length:

>>>
>>> abs(-42)
42

>>> z = 3 + 2j

>>> abs(z)
3.605551275463989

>>> from math import sqrt
>>> sqrt(z.real**2 + z.imag**2)
3.605551275463989

You might remember from an earlier section that a complex number multiplied by its conjugate produces its magnitude squared.

Finding the Distance Between Two Points

Let’s find the Bermuda Triangle’s geometric center and the distances to it from the three cities that form its boundaries. First, you need to sum all coordinates and divide the result by their count to take the average:

geometric_center = sum([miami_fl, san_juan, hamilton]) / 3

This will give you a point located in the Atlantic Ocean, somewhere within the triangle:

Geometric Center of the Bermuda Triangle

Now you can create vectors anchored in the cities and directed toward the geometric center of the triangle. Vectors are made by subtracting the source from the target point:

v1 = geometric_center - miami_fl
v2 = geometric_center - san_juan
v3 = geometric_center - hamilton

Since you subtract complex numbers, every vector is also a complex number made up of two parts. To get your distances, calculate the magnitude of each vector:

>>>
>>> abs(v1)
9.83488994681275

>>> abs(v2)
8.226809506084367

>>> abs(v3)
8.784732429678444

These vector lengths don’t reflect meaningful distances but are good approximations for a toy example like this. To represent accurate results in tangible units, you’d have to convert the coordinates from spherical to rectangular first or calculate the distance using the great circle method instead.

Translating, Flipping, Scaling, and Rotating

It might be bothering you that the triangle appears in the second quadrant of the Cartesian coordinate system. Let’s move it so that its geometric center aligns with the origin. All three vertices will be translated by the length of the vector indicated by the geometric center but in the opposite direction:

triangle = miami_fl, san_juan, hamilton
offset = -geometric_center
centered_triangle = [vertex + offset for vertex in triangle]

Note that you’re adding two complex numbers together, which performs their element-wise addition. This is an affine transformation since it doesn’t change the shape of the triangle or the relative placement of its vertices:

Translation of the Bermuda Triangle

A mirror reflection of the triangle around the real or imaginary axis requires inverting the respective component in its vertices. For example, to flip it horizontally, you’ll have to use the negative of the real part, which corresponds to the horizontal direction. To flip it vertically, you’ll take the negative of the imaginary part:

flipped_horizontally = [complex(-v.real, v.imag) for v in centered_triangle]
flipped_vertically = [complex(v.real, -v.imag) for v in centered_triangle]

The latter is essentially the same as calculating a complex number conjugate, so you can call .conjugate() on each vertex directly to do the hard work for you:

flipped_vertically = [v.conjugate() for v in centered_triangle]

Naturally, there’s nothing to stop you from applying the symmetry in either direction or both directions simultaneously. In such a case, you can use the unary minus operator in front of the complex number to flip its real and imaginary parts:

flipped_in_both_directions = [-v for v in centered_triangle]

Go ahead and fiddle with the different flip combinations using the interactive Jupyter Notebook available in the downloadable materials. Here’s how the triangle will look when you flip it along both axes:

Symmetry of the Bermuda Triangle

Scaling is similar to translating, but instead of adding an offset, you’re going to multiply each vertex by a constant factor, which must be a real number:

scaled_triangle = [1.5*vertex for vertex in centered_triangle]

Doing so results in multiplying both components of each complex number by the same amount. It should stretch your Bermuda Triangle, making it look bigger on the plot:

Scaled Bermuda Triangle

Multiplying the triangle’s vertices by another complex number, on the other hand, has the effect of rotating it around the coordinate system’s origin. That’s vastly different from how you’d typically multiply vectors by each other. For example, a dot product of two vectors will result in a scalar, while their cross product returns a new vector in three-dimensional space, which is perpendicular to the surface they define.

When you multiply the vertices by the imaginary unit, it will rotate the triangle 90° counterclockwise. If you keep repeating it, you’ll eventually arrive where you started:

Rotation of the Bermuda Triangle

How do you find a specific complex number that will rotate another complex number by any desired angle when both are multiplied? First, take a look at the following table, which summarizes the consecutive rotations by 90°:

90° rotations Total angle Formula Exponent Value
0 z j0 1
1 90° z × j j1 j
2 180° z × j × j j2 -1
3 270° z × j × j × j j3 -j
4 360° z × j × j × j × j j4 1
5 450° z × j × j × j × j × j j5 j
6 540° z × j × j × j × j × j × j j6 -1
7 630° z × j × j × j × j × j × j × j j7 -j
8 720° z × j × j × j × j × j × j × j × j j8 1

When you express the repeated multiplication by j in terms of positive integer exponents, then a pattern emerges. Notice how raising the imaginary unit to the subsequent powers makes it cycle through the same values repeatedly. You can extrapolate this onto fractional exponents and expect them to correspond to the intermediate angles.

For example, the exponent halfway through the first rotation is equal to 0.5 and represents a 45° angle:

Rotating a Complex Number

So, if you know that a power of one represents the right angle, and anything in between scales proportionally, then you can derive this generic formula for arbitrary rotations:

def rotate(z: complex, degrees: float) -> complex:
    return z * 1j**(degrees/90)

Note that rotation becomes more natural when you express your complex numbers in polar coordinates, which already describe the angle. You can then take advantage of the exponential form to make the calculations more straightforward:

There are two ways to rotate a number using polar coordinates:

import math, cmath

def rotate1(z: complex, degrees: float) -> complex:
    radius, angle = cmath.polar(z)
    return cmath.rect(radius, angle + math.radians(degrees))

def rotate2(z: complex, degrees: float) -> complex:
    return z * cmath.rect(1, math.radians(degrees))

You can sum angles or multiply your complex number by a unit vector.

You’ll learn more about those in the next section.

Exploring the Math Module for Complex Numbers: cmath

You’ve already seen that some built-in functions like abs() and pow() accept complex numbers, while others don’t. For example, you can’t round() a complex number because such an operation doesn’t make sense:

>>>
>>> round(3 + 2j)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: type complex doesn't define __round__ method

Many advanced mathematical functions such as trigonometric, hyperbolic, or logarithmic functions are available in the standard library. Sadly, even if you know everything about the Python math module, it won’t help because none of its functions support complex numbers. You’ll need to combine it with the cmath module, which defines corresponding functions for complex numbers.

The cmath module redefines all floating-point constants from math so that they’re at your fingertips without the need to import both modules:

>>>
>>> import math, cmath
>>> for name in "e", "pi", "tau", "nan", "inf":
...     print(name, getattr(math, name) == getattr(cmath, name))
...
e True
pi True
tau True
nan False
inf True

Note that nan is a special value which is never equal to anything else, including itself! That’s why you’re seeing a solitary False in the output above. In addition to these, cmath provides two complex counterparts for NaN (not a number) and infinity, with both having zero real parts:

>>>
>>> from cmath import nanj, infj
>>> nanj.real, nanj.imag
(0.0, nan)
>>> infj.real, infj.imag
(0.0, inf)

There are about half as many functions in cmath as there are in the standard math module. Most of them mimic the original behavior, but a few are unique to complex numbers. They will let you do the conversion between two coordinate systems that you’ll explore in this section.

Extracting the Root of a Complex Number

The fundamental theorem of algebra states that a degree n polynomial with complex coefficients has exactly n complex roots. That is quite significant if you think about it, so let it sink in for a moment.

Now, let’s take the quadratic function x2 + 1 as an example. Visually, this parabola doesn’t intersect the X-axis because it lies one unit above the origin. The function’s discriminant is negative, which confirms this observation arithmetically. At the same time, it’s a polynomial of degree two, so it must have two complex roots, even though it doesn’t have any real ones!

To find those roots, you can rewrite the function as a quadratic equation, then move the constant over to the right and take the square root of both sides:

Quadratic Equation

In the domain of real numbers, the square root is only defined for nonnegative input values. Therefore, calling this function in Python will raise an exception with an appropriate error message:

>>>
>>> import math
>>> math.sqrt(-1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: math domain error

However, when you treat √-1 as a complex number instead and call the relevant function from the cmath module, then you’ll get a more meaningful result:

>>>
>>> import cmath
>>> cmath.sqrt(-1)
1j

That makes sense. After all, the intermediate form x2 = -1 of the quadratic equation is the very definition of the imaginary unit. But, wait a minute. Where did the other complex root go? What about complex roots of higher-degree polynomials?

For example, a fourth-degree polynomial x4 + 1, which can be written as an equation x4 = -1, has these four complex roots:

  • z0 = -√2/2 + √2/2j
  • z1 = -√2/2 - √2/2j
  • z2 = √2/2 + √2/2j
  • z3 = √2/2 - √2/2j

Raising each root to the fourth power results in a complex number equal to -1 + 0j or a real number -1:

>>>
>>> import cmath
>>> z0 = -cmath.sqrt(2)/2 + cmath.sqrt(2)/2*1j

>>> z0**4
(-1.0000000000000004-0j)

>>> (z0**4).real
-1.0000000000000004

You’ll notice that the resulting value isn’t exactly -1 due to the rounding error in floating-point arithmetic. To account for that, you can call cmath.isclose() whenever you need to tell if two complex numbers are close in value:

>>>
>>> cmath.isclose(z0**4, -1)
True

Unfortunately, you can’t compute other complex roots with pure Python because regular exponentiation always gives one solution:

>>>
>>> pow(-1, 1/4)
(0.7071067811865476+0.7071067811865475j)

That’s only one of the roots listed before. The mathematical formula for finding all complex roots takes advantage of the trigonometric form of complex numbers:

The Formula for Finding Complex Roots

The r and φ are polar coordinates of the complex number, while n is the polynomial’s degree, and k is the root’s index, starting at zero. The good news is you don’t need to painstakingly calculate those roots yourself. The quickest way to find them is by installing a third-party library such as NumPy and importing it to your project:

>>>
>>> import numpy as np
>>> np.roots([1, 0, 0, 0, 1])  # Coefficients of the polynomial x**4 + 1
array([-0.70710678+0.70710678j, -0.70710678-0.70710678j,
        0.70710678+0.70710678j,  0.70710678-0.70710678j])

Knowing about the various complex number forms and their coordinate systems can be useful. As you can see, it helps in solving practical problems like finding complex roots. So, in the next section, you’ll delve into more details.

Converting Between Rectangular and Polar Coordinates

Geometrically, you can look at a complex number twofold. On the one hand, it’s a point whose horizontal and vertical distances from the origin uniquely identify its location. These are known as rectangular coordinates comprising the real and imaginary parts.

On the other hand, you can describe the same point in polar coordinates that also let you find it unambiguously with two distances:

  1. Radial distance is the length of the radius measured from the origin.
  2. Angular distance is the angle measured between the horizontal axis and the radius.

The radius, also known as the modulus, corresponds to the complex number’s magnitude, or the vector’s length. The angle is commonly referred to as the phase or argument of a complex number. It’s useful to express the angle in radians rather than degrees when working with trigonometric functions.

Here’s a depiction of a complex number in both coordinate systems:

Polar Coordinates

Therefore, a point (3, 2) in the Cartesian coordinate system has a radius of approximately 3.6 and an angle of about 33.7°, or roughly π over 5.4 radians.

The conversion between the two coordinate systems is made possible with a couple of functions buried in the cmath module. Specifically, to get the polar coordinates of a complex number, you must pass it to cmath.polar():

>>>
>>> import cmath
>>> cmath.polar(3 + 2j)
(3.605551275463989, 0.5880026035475675)

It will return a tuple, where the first element is the radius and the second element is the angle in radians. Note that the radius has the same value as the magnitude, which you can calculate by calling abs() on your complex number. Conversely, if you were only interested in getting the angle of a complex number, then you could call cmath.phase():

>>>
>>> z = 3 + 2j

>>> abs(z)  # Magnitude is also the radial distance
3.605551275463989

>>> import cmath
>>> cmath.phase(3 + 2j)
0.5880026035475675

>>> cmath.polar(z) == (abs(z), cmath.phase(z))
True

The angle can be obtained using basic trigonometry since the real part, the imaginary part, and the magnitude together form a right triangle:

Using the Inverse Trigonometric Functions

You can use the inverse trigonometric functions, such as arcsine, either from math or cmath, but the latter will produce complex values with the imaginary part equal to zero:

>>>
>>> z = 3 + 2j

>>> import math
>>> math.acos(z.real / abs(z))
0.5880026035475675
>>> math.asin(z.imag / abs(z))
0.5880026035475676
>>> math.atan(z.imag / z.real)  # Prefer math.atan2(z.imag, z.real)
0.5880026035475675

>>> import cmath
>>> cmath.acos(z.real / abs(z))
(0.5880026035475675-0j)

There’s one small detail to be careful about when using the arctangent function, though, which led many programming languages to develop an alternative implementation called atan2(). Calculating the ratio between the imaginary and the real part can sometimes produce a singularity due to, for instance, division by zero. Moreover, the individual signs of the two values are lost in the process, making it impossible to tell the angle with certainty:

>>>
>>> import math

>>> math.atan(1 / 0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

>>> math.atan2(1, 0)
1.5707963267948966

>>> math.atan(1 / 1) == math.atan(-1 / -1)
True

>>> math.atan2(1, 1) == math.atan2(-1, -1)
False

Notice how atan() fails to recognize two different points located in the opposite quadrants of the coordinate system. On the other hand, atan2() expects two arguments instead of one to preserve the individual signs before dividing one by another and avoids other problems as well.

To get degrees instead of radians, you can make the necessary conversion using the math module again:

>>>
>>> import math
>>> math.degrees(0.5880026035475675)  # Radians to degrees
33.690067525979785
>>> math.radians(180)  # Degrees to radians
3.141592653589793

Reversing the process—that is, converting polar to rectangular coordinates—relies on another function. However, you can’t just pass the same tuple that you got from cmath.polar() since cmath.rect() expects two separate arguments:

>>>
>>> cmath.rect(cmath.polar(3 + 2j))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: rect expected 2 arguments, got 1

It’s a good idea to unpack the tuple first when doing an assignment and give those elements more descriptive names. Now you can call cmath.rect() correctly:

>>>
>>> radius, angle = cmath.polar(3 + 2j)
>>> cmath.rect(radius, angle)
(3+1.9999999999999996j)

You might encounter rounding errors along the way while Python makes the calculations. Behind the scenes, it calls the trigonometric functions to retrieve the real and imaginary parts:

>>>
>>> import math
>>> radius*(math.cos(angle) + math.sin(angle)*1j)
(3+1.9999999999999996j)

>>> import cmath
>>> radius*(cmath.cos(angle) + cmath.sin(angle)*1j)
(3+1.9999999999999996j)

Again, it doesn’t matter whether you use math or cmath in this case as the results will be identical.

Representing Complex Numbers Differently

Regardless of the coordinate system, you can express the same complex number in a few mathematically equivalent forms:

  • Algebraic (standard)
  • Geometric
  • Trigonometric
  • Exponential

This list isn’t exhaustive as there are more representations, such as the matrix representation of complex numbers.

Having the choice lets you pick the most convenient one to tackle a given problem. For example, you’re going to need the exponential form to calculate discrete Fourier transform in an upcoming section. Using this form is also suitable for multiplying and dividing complex numbers.

Here’s a quick rundown of the individual complex number forms and their coordinates:

Form Rectangular Polar
Algebraic z = x + yj -
Geometric z = (x, y) z = (r, φ)
Trigonometric z = |z|(cos(x/|z|) + jsin(y/|z|)) z = r(cos(φ) + jsin(φ))
Exponential z = |z|eatan2(y/x)j z = r(ejφ)

The algebraic form is native to Python when you specify complex numbers using their literals. You can also view them as points on a Euclidean plane in the Cartesian or polar coordinate systems. While there aren’t separate representations for the trigonometric or exponential form in Python, you can verify if mathematical principles hold.

For example, plugging in Euler’s formula to the trigonometric form will turn it into the exponential one. You can either call the cmath module’s exp() or raise the e constant to a power to get the same result:

>>>
>>> import cmath

>>> algebraic = 3 + 2j
>>> geometric = complex(3, 2)
>>> radius, angle = cmath.polar(algebraic)
>>> trigonometric = radius * (cmath.cos(angle) + 1j*cmath.sin(angle))
>>> exponential = radius * cmath.exp(1j*angle)

>>> for number in algebraic, geometric, trigonometric, exponential:
...     print(format(number, "g"))
...
3+2j
3+2j
3+2j
3+2j

All forms are indeed different ways of encoding the same number. However, you can’t compare them directly because of the rounding errors that may occur in the meantime. Use cmath.isclose() for a safe comparison or format() the numbers as strings appropriately. You’ll find out how to format such strings in the upcoming section.

The explanation of why different forms of a complex number are equivalent requires calculus and goes far beyond the scope of this tutorial. However, if you’re interested in math, then you’ll find the connections between different fields of mathematics that are exhibited by complex numbers to be fascinating.

Dissecting a Complex Number in Python

You’ve already learned a bunch about Python complex numbers and have seen preliminary examples. However, before moving further, it’s worthwhile to cover some final topics. In this section, you’re going to look into comparing complex numbers, formatting strings that contain them, and more.

Testing Equality of Complex Numbers

Mathematically, two complex numbers are equal when they have identical values irrespective of the adopted coordinate system. However, converting between polar and rectangular coordinates typically introduces rounding errors in Python, so you need to watch out for minute differences when comparing them.

For example, when you consider a point on a unit circle whose radius is equal to one and is tilted at 60°, then the trigonometry works out nicely, making the conversion with pen and paper straightforward:

>>>
>>> import math, cmath

>>> z1 = cmath.rect(1, math.radians(60))
>>> z2 = complex(0.5, math.sqrt(3)/2)

>>> z1 == z2
False

>>> z1.real, z2.real
(0.5000000000000001, 0.5)
>>> z1.imag, z2.imag
(0.8660254037844386, 0.8660254037844386)

Even though you know that z1 and z2 are the same point, Python can’t determine that because of the rounding errors. Fortunately, the PEP 485 document defined functions for approximate equality, which are available in the math and cmath modules:

>>>
>>> math.isclose(z1.real, z2.real)
True

>>> cmath.isclose(z1, z2)
True

Remember to always use them when comparing complex numbers! If the default tolerance isn’t good enough for your calculations, you can change it by specifying additional arguments.

Ordering Complex Numbers

If you’re familiar with tuples, then you know that Python can sort them:

>>>
>>> planets = [
...     (6, "saturn"),
...     (4, "mars"),
...     (1, "mercury"),
...     (5, "jupiter"),
...     (8, "neptune"),
...     (3, "earth"),
...     (7, "uranus"),
...     (2, "venus"),
... ]
>>> from pprint import pprint
>>> pprint(sorted(planets))
[(1, 'mercury'),
 (2, 'venus'),
 (3, 'earth'),
 (4, 'mars'),
 (5, 'jupiter'),
 (6, 'saturn'),
 (7, 'uranus'),
 (8, 'neptune')]

By default, the individual tuples are compared left to right:

>>>
>>> (6, "saturn") < (4, "mars")
False
>>> (3, "earth") < (3, "moon")
True

In the first case, the number 6 is greater than 4, so the planet names aren’t considered at all. They can help resolve a tie, though. However, that’s not the case with complex numbers since they don’t define a natural ordering relation. For example, you’ll get an error if you try to compare two complex numbers:

>>>
>>> (3 + 2j) < (2 + 3j)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'complex' and 'complex'

Should the imaginary dimension have more weight than the real one? Should their magnitudes be compared? It’s up to you, and the answers will vary. Since you can’t compare complex numbers directly, you need to tell Python how to sort them by specifying a custom key function, such as abs():

>>>
>>> cities = {
...     complex(-64.78303, 32.2949): "Hamilton",
...     complex(-66.105721, 18.466333): "San Juan",
...     complex(-80.191788, 25.761681): "Miami"
... }

>>> for city in sorted(cities, key=abs, reverse=True):
...     print(abs(city), cities[city])
...
84.22818453809096 Miami
72.38647347392259 Hamilton
68.63651945864338 San Juan

This will sort the complex numbers by their magnitude in descending order.

Formatting Complex Numbers as Strings

There aren’t any format codes specific to complex numbers, but you can format their real and imaginary parts separately using standard codes for floating-point numbers. Below, you’ll find a few techniques that demonstrate this. Some of them will actually apply your format specifier to both the real and imaginary parts in one go.

Let’s take the following complex number as an example and format it with two decimal places on both parts:

>>>
>>> z = pow(3 + 2j, 0.5)
>>> print(z)
(1.8173540210239707+0.5502505227003375j)

A quick way to do this is either by calling format() with a numeric format specifier or by creating an appropriately formatted f-string:

>>>
>>> format(z, ".2f")
'1.82+0.55j'

>>> f"{z:.2f}"
'1.82+0.55j'

If you want more control, for example, to add extra padding around the plus operator, then the f-string will be a better choice:

>>>
>>> f"{z.real:.2f} + {z.imag:.2f}j"
'1.82 + 0.55j'

You can also call .format() on a string object and pass positional or keyword arguments to it:

>>>
>>> "{0:.2f} + {0:.2f}j".format(z.real, z.imag)
'1.82 + 1.82j'

>>> "{re:.2f} + {im:.2f}j".format(re=z.real, im=z.imag)
'1.82 + 0.55j'

The positional arguments provide a sequence of values, while the keyword arguments let you refer to them by name. Similarly, you can use the string modulo operator (%) with either a tuple or a dictionary:

>>>
>>> "%.2f + %.2fj" % (z.real, z.imag)
'1.82 + 0.55j'

>>> "%(re).2f + %(im).2fj" % {"re": z.real, "im": z.imag}
'1.82 + 0.55j'

However, this uses a different placeholder syntax and is slightly old-fashioned.

Creating Your Own Complex Data Type

The Python data model defines a set of special methods that you can implement to make your classes compatible with certain built-in types. Say you were working with points and vectors and wanted to get the angle between two bound vectors. You might calculate their dot product and do some trigonometry. Alternatively, you can take advantage of complex numbers.

Let’s define your classes first:

from typing import NamedTuple

class Point(NamedTuple):
    x: float
    y: float

class Vector(NamedTuple):
    start: Point
    end: Point

A Point has the x and y coordinates, while a Vector connects two points. You might remember cmath.phase(), which calculates the angular distance of a complex number. Now, if you treated your vectors as complex numbers and knew their phases, then you could subtract them to obtain the desired angle.

To make Python recognize vector instances as complex numbers, you have to supply .__complex__() in the class body:

class Vector(NamedTuple):
    start: Point
    end: Point

    def __complex__(self):
        real = self.end.x - self.start.x
        imag = self.end.y - self.start.y
        return complex(real, imag)

The code inside must always return an instance of the complex data type, so it typically constructs a new complex number out of your object. Here, you’re subtracting the initial and terminal points to get the horizontal and vertical displacements, which serve as the real and imaginary parts. The method will run through delegation when you call the global complex() on a vector instance:

>>>
>>> vector = Vector(Point(-2, -1), Point(1, 1))
>>> complex(vector)
(3+2j)

In some cases, you don’t have to make this kind of type casting yourself. Let’s see an example in practice:

>>>
>>> v1 = Vector(Point(-2, -1), Point(1, 1))
>>> v2 = Vector(Point(10, -4), Point(8, -1))

>>> import math, cmath
>>> math.degrees(cmath.phase(v2) - cmath.phase(v1))
90.0

You’ve got two vectors identified by four distinct points. Next, you pass them directly to cmath.phase(), which does the conversion to a complex number for you and returns the phase. The phase difference is the angle between the two vectors.

Wasn’t that beautiful? You’ve saved yourself from typing a lot of error-prone code by piggybacking on the complex numbers and a bit of Python magic.

Calculating the Discrete Fourier Transform With Complex Numbers

While you can use real numbers to calculate the sine and cosine coefficients of a periodic function’s frequencies with the Fourier transform, it’s usually more convenient to deal with only one complex coefficient per frequency. The discrete Fourier transform in the complex domain is given by the following formula:

The Discrete Fourier Transform

For each frequency bin k, it measures the correlation of the signal and a particular sine wave expressed as a complex number in the exponential form. (Thank you, Leonhard Euler!) The angular frequency of the wave can be calculated by multiplying the round angle, which is 2π radians, by k over the number of discrete samples:

The Angular Frequency

Coding this in Python looks quite neat when you take advantage of the complex data type:

from cmath import pi, exp

def discrete_fourier_transform(x, k):
    omega = 2 * pi * k / (N := len(x))
    return sum(x[n] * exp(-1j * omega * n) for n in range(N))

This function is a literal transcription of the formulas above. Now you can run a frequency analysis on a sound that you load from an audio file using Python’s wave module or that you synthesize from scratch. One of the Jupyter Notebooks accompanying this tutorial lets you play with audio synthesis and analysis interactively.

To plot the frequency spectrum with Matplotlib, you must know the sampling frequency, which determines your frequency bin resolution as well as the Nyquist limit:

import matplotlib.pyplot as plt

def plot_frequency_spectrum(
    samples,
    samples_per_second,
    min_frequency=0,
    max_frequency=None,
):
    num_bins = len(samples) // 2
    nyquist_frequency = samples_per_second // 2

    magnitudes = []
    for k in range(num_bins):
        magnitudes.append(abs(discrete_fourier_transform(samples, k)))

    # Normalize magnitudes
    magnitudes = [m / max(magnitudes) for m in magnitudes]

    # Calculate frequency bins
    bin_resolution = samples_per_second / len(samples)
    frequency_bins = [k * bin_resolution for k in range(num_bins)]

    plt.xlim(min_frequency, max_frequency or nyquist_frequency)
    plt.bar(frequency_bins, magnitudes, width=bin_resolution)

The number of frequency bins in the spectrum is equal to half the samples, while the Nyquist frequency limits the highest frequency you can measure. The transform returns a complex number whose magnitude corresponds to the amplitude of a sine wave at the given frequency, whereas its angle is the phase.

Here’s a sample frequency plot of a sound wave comprising three tones—440 Hz, 1.5 kHz, and 5 kHz—having equal amplitudes:

Frequency Spectrum
Frequency spectrum plot

Note this was a purely academic example since calculating the discrete Fourier transform with nested iterations has O(n2) time complexity, making it unusable in practice. For real-life applications, you want to use the fast Fourier transform (FFT) algorithm best implemented in a C library, such as the FFT in SciPy.

Conclusion

The ease of using complex numbers in Python makes them a surprisingly fun and practical tool. You saw two-dimensional vectors implemented practically for free, and you were able to analyze sound frequencies thanks to them. Complex numbers let you elegantly express mathematical formulas in code without much boilerplate syntax standing in the way.

In this tutorial, you learned how to:

  • Define complex numbers with literals in Python
  • Represent complex numbers in rectangular and polar coordinates
  • Use complex numbers in arithmetic expressions
  • Take advantage of the built-in cmath module
  • Translate mathematical formulas directly to Python code

What has your experience been with Python complex numbers so far? Were you ever intimidated by them? What other interesting problems do you think they’ll let you solve?

You can click the link below to get the full source code for this tutorial:

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Bartosz Zaczyński

Bartosz Zaczyński Bartosz Zaczyński

Bartosz is a bootcamp instructor, author, and polyglot programmer in love with Python. He helps his students get into software engineering by sharing over a decade of commercial experience in the IT industry.

» More about Bartosz

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Join us and get access to hundreds of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Join us and get access to hundreds of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Real Python Comment Policy: The most useful comments are those written with the goal of learning from or helping out other readers—after reading the whole article and all the earlier comments. Complaints and insults generally won’t make the cut here.

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Keep Learning

Related Tutorial Categories: intermediate python