Python - Complex Numbers
Python includes complex numbers as a built-in numeric type, sitting alongside integers and floats. This isn't a bolted-on afterthought—complex numbers are deeply integrated into the language,...
Key Insights
- Python treats complex numbers as first-class citizens with built-in support, using
jnotation for the imaginary unit and providing intuitive arithmetic operations that work seamlessly with other numeric types. - The
cmathmodule extends standard math functions to the complex domain, enabling operations like square roots of negative numbers and conversions between rectangular and polar coordinate forms. - For performance-critical applications involving large datasets of complex numbers, NumPy’s complex arrays offer vectorized operations that dramatically outperform Python’s built-in type.
Introduction to Complex Numbers in Python
Python includes complex numbers as a built-in numeric type, sitting alongside integers and floats. This isn’t a bolted-on afterthought—complex numbers are deeply integrated into the language, supporting all standard arithmetic operations and working naturally with other numeric types.
The notation uses j for the imaginary unit, not i. This follows electrical engineering convention, where i typically represents current. If you’re coming from a mathematics background, this takes some adjustment, but you’ll adapt quickly.
Engineers and scientists encounter complex numbers constantly: signal processing relies on them for Fourier transforms, electrical engineers use them for AC circuit analysis, control systems engineers need them for transfer functions, and physicists use them throughout quantum mechanics. Python’s native support makes it a practical choice for these domains.
# Creating complex numbers with literal notation
z1 = 3 + 4j
z2 = -2.5 + 1.7j
z3 = 6j # Pure imaginary number
z4 = 5 + 0j # Real number as complex
# Using the complex() constructor
z5 = complex(3, 4) # Equivalent to 3 + 4j
z6 = complex(7) # Equivalent to 7 + 0j
z7 = complex('3+4j') # Parsing from string (no spaces allowed)
print(f"z1 = {z1}") # Output: z1 = (3+4j)
print(f"z5 = {z5}") # Output: z5 = (3+4j)
print(f"Type: {type(z1)}") # Output: Type: <class 'complex'>
Note the string parsing quirk: complex('3+4j') works, but complex('3 + 4j') raises a ValueError. Strip whitespace before parsing user input.
Basic Operations and Arithmetic
Complex arithmetic in Python works exactly as you’d expect mathematically. All standard operators function correctly, and Python handles type coercion intelligently when mixing complex numbers with integers or floats.
z1 = 3 + 4j
z2 = 1 - 2j
# Basic arithmetic
addition = z1 + z2 # (4+2j)
subtraction = z1 - z2 # (2+6j)
multiplication = z1 * z2 # (3*1 - 4*(-2)) + (3*(-2) + 4*1)j = (11-2j)
division = z1 / z2 # (-1+2j)
print(f"Addition: {addition}")
print(f"Subtraction: {subtraction}")
print(f"Multiplication: {multiplication}")
print(f"Division: {division}")
# Exponentiation
squared = z1 ** 2 # (-7+24j)
cubed = z1 ** 3 # (-117+44j)
# Mixed operations with real numbers
scaled = z1 * 2 # (6+8j)
shifted = z1 + 5 # (8+4j)
mixed = z1 * 2.5 + 3 # (10.5+10j)
print(f"Scaled: {scaled}")
print(f"Mixed: {mixed}")
# Absolute value (magnitude/modulus)
magnitude = abs(z1) # sqrt(3² + 4²) = 5.0
print(f"Magnitude of {z1}: {magnitude}")
Python automatically promotes integers and floats to complex when needed. The expression 3 + 4j + 2 evaluates correctly because Python converts 2 to 2+0j before addition.
One gotcha: floor division (//) and modulo (%) don’t work with complex numbers. Python raises a TypeError because these operations aren’t mathematically defined for complex values.
z = 3 + 4j
# These raise TypeError
# z // 2 # TypeError: can't take floor of complex number
# z % 2 # TypeError: can't mod complex numbers
Accessing Real and Imaginary Parts
Every complex number exposes its components through .real and .imag attributes. These are read-only floats—you cannot modify them directly.
z = 3 + 4j
# Accessing components
real_part = z.real # 3.0
imag_part = z.imag # 4.0
print(f"Real: {real_part}, Imaginary: {imag_part}")
# Components are always floats
print(f"Types: {type(real_part)}, {type(imag_part)}") # float, float
# Computing the conjugate
conjugate = z.conjugate() # (3-4j)
print(f"Conjugate of {z}: {conjugate}")
# Useful property: z * conjugate(z) = |z|²
product = z * z.conjugate() # (25+0j)
magnitude_squared = abs(z) ** 2 # 25.0
print(f"z * conj(z) = {product}")
print(f"|z|² = {magnitude_squared}")
The conjugate has practical importance. Multiplying a complex number by its conjugate yields a real number equal to the magnitude squared. This property is essential in signal processing for computing power spectral density and in numerical methods for dividing complex numbers.
# Manual complex division using conjugate
def divide_complex(z1, z2):
"""Divide z1 by z2 using conjugate multiplication."""
conjugate = z2.conjugate()
numerator = z1 * conjugate
denominator = (z2 * conjugate).real # Always real
return complex(numerator.real / denominator,
numerator.imag / denominator)
result = divide_complex(3 + 4j, 1 - 2j)
print(f"Manual division: {result}") # (-1+2j)
The cmath Module
The standard math module fails with complex numbers—it raises ValueError for operations like sqrt(-1). The cmath module provides complex-aware versions of mathematical functions.
import cmath
import math
# Square root of negative numbers
sqrt_negative = cmath.sqrt(-1) # 1j
print(f"sqrt(-1) = {sqrt_negative}")
# Exponential and logarithm
z = 1 + 1j
exp_z = cmath.exp(z) # e^(1+j)
log_z = cmath.log(z) # ln(1+j)
print(f"e^{z} = {exp_z}")
print(f"ln({z}) = {log_z}")
# Trigonometric functions
sin_z = cmath.sin(z)
cos_z = cmath.cos(z)
print(f"sin({z}) = {sin_z}")
print(f"cos({z}) = {cos_z}")
The polar coordinate functions deserve special attention. Engineers frequently convert between rectangular (a + bj) and polar (r, θ) forms.
import cmath
z = 3 + 4j
# Convert to polar coordinates
r, theta = cmath.polar(z)
print(f"Polar: r={r}, θ={theta} radians") # r=5.0, θ≈0.927
# Get just the phase angle
phase = cmath.phase(z) # Same as theta above
print(f"Phase: {phase} radians, {math.degrees(phase)} degrees")
# Convert back to rectangular
z_reconstructed = cmath.rect(r, theta)
print(f"Rectangular: {z_reconstructed}") # (3+4j)
# Euler's formula: e^(iθ) = cos(θ) + i*sin(θ)
theta = math.pi / 4 # 45 degrees
euler = cmath.exp(1j * theta)
manual = math.cos(theta) + 1j * math.sin(theta)
print(f"e^(iπ/4) = {euler}")
print(f"cos(π/4) + i*sin(π/4) = {manual}")
Practical Applications
Complex numbers appear throughout engineering and scientific computing. Here are two concrete examples.
Electrical Impedance Calculation
In AC circuits, impedance combines resistance, capacitance, and inductance into a single complex quantity. This simplifies circuit analysis dramatically.
import cmath
import math
def calculate_impedance(R, L, C, frequency):
"""
Calculate total impedance for series RLC circuit.
R: Resistance (ohms)
L: Inductance (henries)
C: Capacitance (farads)
frequency: Signal frequency (Hz)
"""
omega = 2 * math.pi * frequency
# Individual impedances
Z_R = R + 0j # Resistor: purely real
Z_L = 1j * omega * L # Inductor: positive imaginary
Z_C = -1j / (omega * C) # Capacitor: negative imaginary
# Total series impedance
Z_total = Z_R + Z_L + Z_C
# Convert to polar for magnitude and phase
magnitude, phase = cmath.polar(Z_total)
return {
'impedance': Z_total,
'magnitude': magnitude,
'phase_degrees': math.degrees(phase),
'Z_R': Z_R,
'Z_L': Z_L,
'Z_C': Z_C
}
# Example: 100Ω resistor, 10mH inductor, 100μF capacitor at 60Hz
result = calculate_impedance(R=100, L=0.01, C=100e-6, frequency=60)
print(f"Total impedance: {result['impedance']:.2f}")
print(f"Magnitude: {result['magnitude']:.2f} Ω")
print(f"Phase: {result['phase_degrees']:.2f}°")
Mandelbrot Set Membership Test
The Mandelbrot set provides a visually striking application of complex iteration.
def mandelbrot_iterations(c, max_iter=100):
"""
Determine if complex number c is in the Mandelbrot set.
Returns iteration count when escaped, or max_iter if bounded.
"""
z = 0 + 0j
for n in range(max_iter):
if abs(z) > 2:
return n # Escaped
z = z * z + c
return max_iter # Likely in set
# Test some points
test_points = [
0 + 0j, # In set (origin)
-1 + 0j, # In set
0.5 + 0j, # Not in set
-0.5 + 0.5j, # In set
1 + 1j, # Not in set
]
for c in test_points:
iters = mandelbrot_iterations(c)
status = "in set" if iters == 100 else f"escaped at {iters}"
print(f"c = {c}: {status}")
Performance Considerations and Alternatives
Python’s built-in complex type works well for individual calculations but becomes a bottleneck when processing large datasets. Each complex number is a full Python object with associated memory overhead and method dispatch costs.
For numerical computing, NumPy’s complex arrays provide vectorized operations that execute in compiled C code.
import numpy as np
import time
# Create large arrays of complex numbers
size = 1_000_000
# NumPy approach
np_array = np.random.random(size) + 1j * np.random.random(size)
# Pure Python approach
py_list = [complex(np.random.random(), np.random.random())
for _ in range(size)]
# Benchmark: compute magnitude of all elements
start = time.perf_counter()
np_magnitudes = np.abs(np_array)
np_time = time.perf_counter() - start
start = time.perf_counter()
py_magnitudes = [abs(z) for z in py_list]
py_time = time.perf_counter() - start
print(f"NumPy time: {np_time:.4f}s")
print(f"Python time: {py_time:.4f}s")
print(f"Speedup: {py_time / np_time:.1f}x")
# NumPy complex operations
z_array = np.array([1+2j, 3+4j, 5+6j])
print(f"Real parts: {z_array.real}")
print(f"Imaginary parts: {z_array.imag}")
print(f"Conjugates: {np.conj(z_array)}")
print(f"Magnitudes: {np.abs(z_array)}")
print(f"Phases: {np.angle(z_array)}")
NumPy typically delivers 50-100x speedups for bulk operations. Use Python’s built-in complex type for individual calculations and algorithm prototyping; switch to NumPy when processing arrays or when performance matters.
Summary
Python’s complex number support is comprehensive and practical. The built-in type handles everyday calculations elegantly, while cmath extends mathematical functions to the complex domain. For production numerical code, NumPy’s complex arrays provide the performance needed for real-world applications.
Key practices to remember: use j notation for literals, rely on cmath instead of math for complex operations, extract components via .real and .imag attributes, and convert to polar form with cmath.polar() when working with magnitudes and phases. When performance becomes critical, migrate to NumPy’s vectorized operations.