Complex numbers in Python are first-class citizens, represented by the complex data type. They are an essential tool for scientific computing, electrical engineering, signal processing, and any domain that requires modeling phenomena in two dimensions. Unlike some other languages, Python has built-in support for complex numbers, making them as easy to use as integers or floats.

The complex Data Type and Literals

A complex number is defined by a pair of floating-point numbers: a real part and an imaginary part. In Python, the imaginary unit is denoted by the letter j or J, not the traditional mathematical i. This convention was adopted from electrical engineering, where i is commonly used to represent electrical current.

You can create a complex number literal by appending j to a numeric literal. The real part is optional; if omitted, it defaults to 0.0.

# Literal notation for complex numbers
z1 = 3 + 4j      # Real part is 3.0, imaginary part is 4.0
z2 = 2j          # Real part is 0.0, imaginary part is 2.0
z3 = -7 - 1.5j   # Negative components are allowed
z4 = 4.2e-10 + 1.23J  # Scientific notation is also supported

print(type(z1))  # Output: <class 'complex'>
print(z1)        # Output: (3+4j)
print(z2)        # Output: 2j

You can also create them using the complex() constructor, which takes two arguments (real, imag) or a single string.

# Using the complex constructor
z5 = complex(3, 4)     # Same as 3+4j
z6 = complex(0, 2)     # Same as 2j
z7 = complex('5-2j')   # Parsing from a string (note: no spaces around the operator)

print(z5, z6, z7)  # Output: (3+4j) 2j (5-2j)

Accessing Real and Imaginary Parts

The real and imaginary parts of a complex number are accessible as attributes. These are read-only float values. Attempting to assign to them will raise an AttributeError.

z = 3 + 4j
real_part = z.real  # Returns 3.0
imag_part = z.imag  # Returns 4.0

print(f"Real: {real_part}, Imaginary: {imag_part}")
# Output: Real: 3.0, Imaginary: 4.0

# z.real = 5.0  # This would raise: AttributeError: readonly attribute

Basic Arithmetic Operations

The standard arithmetic operators (+, -, *, /, **) work intuitively with complex numbers. The operations follow the algebraic rules for complex numbers.

a = 1 + 2j
b = 3 - 4j

# Addition: (a + c) + (b + d)j
sum_ab = a + b  # (1+3) + (2-4)j -> (4-2j)

# Subtraction: (a - c) + (b - d)j
diff_ab = a - b # (1-3) + (2-(-4))j -> (-2+6j)

# Multiplication: (a*c - b*d) + (a*d + b*c)j
prod_ab = a * b # (1*3 - 2*(-4)) + (1*(-4) + 2*3)j -> (3+8) + (-4+6)j -> (11+2j)

# Division: ( (a*c + b*d) / (c² + d²) ) + ( (b*c - a*d) / (c² + d²) )j
quot_ab = a / b # Complex division formula

print(f"Sum: {sum_ab}")      # Output: Sum: (4-2j)
print(f"Product: {prod_ab}") # Output: Product: (11+2j)
print(f"Quotient: {quot_ab}") # Output: Quotient: (-0.2+0.4j)

The cmath Module: Advanced Mathematical Functions

The built-in math module does not support complex numbers. For advanced mathematical operations, you must use the cmath module. It provides complex-aware versions of most common mathematical functions, such as sqrt, exp, log, and trigonometric functions like sin, cos.

import cmath

z = 1 + 1j

# Square root of a complex number
sqrt_z = cmath.sqrt(z)
print(f"sqrt({z}) = {sqrt_z}")  # Output: sqrt((1+1j)) = (1.09868411346781+0.45508986056222733j)

# Exponential function: e^z
exp_z = cmath.exp(z)
print(f"exp({z}) = {exp_z}")    # Output: exp((1+1j)) = (1.4686939399158851+2.2873552871788423j)

# Natural logarithm
log_z = cmath.log(z)
print(f"log({z}) = {log_z}")    # Output: log((1+1j)) = (0.34657359027997264+0.7853981633974483j)

# Trigonometric functions
sin_z = cmath.sin(z)
cos_z = cmath.cos(z)
print(f"sin({z}) = {sin_z}")    # Output: sin((1+1j)) = (1.2984575814159773+0.6349639147847361j)

Phase (Argument) and Polar Coordinates

A complex number can also be represented in polar coordinates using its magnitude (absolute value) and its phase (angle from the positive real axis). The cmath module provides functions to work with this representation.

z = 1 + 1j

# Get the magnitude (radius) - same as abs(z)
magnitude = abs(z)  # sqrt(1² + 1²) = sqrt(2) ≈ 1.414
print(f"Magnitude: {magnitude}")

# Get the phase (angle in radians) - between -π and π
phase = cmath.phase(z)  # arctan(1/1) = π/4 ≈ 0.7854 radians
print(f"Phase (radians): {phase}")
print(f"Phase (degrees): {cmath.phase(z) * 180 / cmath.pi}")

# Convert from polar coordinates back to rectangular
new_z = cmath.rect(magnitude, phase) # Should reconstruct (1+1j)
print(f"From polar: {new_z}") # Output: From polar: (1.0000000000000002+1j) (note tiny floating-point error)

Common Pitfalls and Best Practices

  1. The Imaginary Unit j: The most common mistake is using i instead of j. Remember, Python requires the suffix j to denote an imaginary number.
  2. String Parsing with complex(): When using complex(string), the string must not contain any whitespace between the operator and the numbers (e.g., '3 + 4j' will fail, but '3+4j' will work).
  3. Floating-Point Precision: The real and imaginary parts are stored as floats, so they are subject to the same precision limitations and rounding errors as the float type. For exact representations, consider the symbolic mathematics library sympy.
  4. Using the Wrong Module: Attempting to use functions from the math module on a complex number will raise a TypeError. Always use cmath for complex-aware operations.
    import math
    z = 1+2j
    # math.sqrt(z)  # This will raise: TypeError: can't convert complex to float
    
  5. Branch Cuts: Functions like sqrt and log have branch cuts, which are curves in the complex plane across which the function is discontinuous. The cmath module implementations follow standard conventions for these branch cuts, but it’s important to be aware of them for advanced work to avoid unexpected results.