Python

Trigonometric Functions in Python: A Complete Guide

Python · Published 2026-10-24

An early nineteenth-century brass protractor with a pivoting arm
A brass protractor with a pivoting arm, made between 1814 and 1820.

Python's trigonometric functions live in the standard math module (math.sin, math.cos, math.tan and their inverses), they always work in radians, and they return floats that are only approximately exact. Convert degrees with math.radians(), use math.atan2() instead of math.atan() for angles from coordinates, and switch to NumPy for arrays or cmath for complex numbers.

The basic functions: sin, cos, tan

Import math and pass an angle in radians. The constant math.pi is the double-precision approximation of π.

import math

print(math.sin(math.pi / 6))   # 30 degrees
print(math.cos(0))
print(math.tan(math.pi / 4))   # 45 degrees
0.49999999999999994
1.0
0.9999999999999999

Notice that sin(π/6) is not printed as exactly 0.5. That is not a bug in Python; it is binary floating point at work, covered in detail below. Python also provides math.tau (2π), which makes full-turn arithmetic easier to read.

Radians vs degrees

The single most common mistake is passing degrees directly. math.sin(30) computes the sine of 30 radians, which is a completely different number:

print(math.sin(30))                  # 30 radians, almost certainly not what you meant
print(math.sin(math.radians(30)))    # 30 degrees
print(math.degrees(math.pi / 3))     # radians back to degrees
-0.9880316240928618
0.49999999999999994
59.99999999999999

math.radians(x) multiplies by π/180 and math.degrees(x) multiplies by 180/π. Convert at the boundary of your program (where user input or file data arrives) and keep everything in radians internally; mixing units inside a calculation is where bugs hide.

Inverse functions and their ranges

math.asin, math.acos and math.atan return an angle in radians. Because sine, cosine and tangent are periodic, each inverse returns only one "principal" value from a fixed range:

FunctionValid inputOutput range (radians)Output range (degrees)
math.asin(x)−1 ≤ x ≤ 1[−π/2, π/2][−90, 90]
math.acos(x)−1 ≤ x ≤ 1[0, π][0, 180]
math.atan(x)any real(−π/2, π/2)(−90, 90)
math.atan2(y, x)any reals[−π, π][−180, 180]
print(math.degrees(math.asin(0.5)))
print(math.degrees(math.acos(-1)))
print(math.degrees(math.atan(1)))

try:
    math.asin(1.0000001)
except ValueError as e:
    print("ValueError:", e)
30.000000000000004
180.0
45.0
ValueError: math domain error

Inputs outside [−1, 1] raise ValueError (the exact message text differs between Python versions). This bites in practice when a value that should be exactly 1, such as a normalized dot product, comes out as 1.0000000000000002 after rounding. Clamp before calling:

def safe_acos(x):
    return math.acos(max(-1.0, min(1.0, x)))

atan2 vs atan

math.atan(y / x) loses information: the ratio y / x is the same for the points (1, 1) and (−1, −1), so atan cannot tell which quadrant you are in. It also divides by zero when x == 0. math.atan2(y, x) takes the two components separately and returns the correct angle in all four quadrants. Note the argument order: y first.

for x, y in [(1, 1), (-1, 1), (-1, -1), (1, -1)]:
    a = math.degrees(math.atan(y / x))
    b = math.degrees(math.atan2(y, x))
    print((x, y), round(a, 1), round(b, 1))

print(math.atan2(1, 0))     # x == 0 is fine: pi/2
(1, 1) 45.0 45.0
(-1, 1) -45.0 135.0
(-1, -1) 45.0 -135.0
(1, -1) -45.0 -45.0
1.5707963267948966

If you need an angle in [0, 360) rather than [−180, 180], apply % 360 to the degree value (or % math.tau to radians).

hypot and dist

math.hypot(x, y) returns √(x² + y²), the length of a vector or hypotenuse. It avoids intermediate overflow and underflow that a naive math.sqrt(x*x + y*y) can hit with very large or very small values. Since Python 3.8 it accepts any number of coordinates, and math.dist(p, q) gives the Euclidean distance between two points.

print(math.hypot(3, 4))
print(math.hypot(1, 2, 2))            # 3-D length, Python 3.8+
print(math.dist((1, 2), (4, 6)))      # Python 3.8+
5.0
3.0
5.0

Together, atan2 and hypot convert Cartesian coordinates to polar form: angle = atan2(y, x), radius = hypot(x, y).

Floating-point results: why sin(π) is not 0

math.pi is the closest double to π, not π itself, so functions evaluated at "special" angles return tiny residues instead of exact zeros:

print(math.sin(math.pi))
print(math.cos(math.pi / 2))
print(math.tan(math.pi / 2))   # not infinity, just very large
1.2246467991473532e-16
6.123233995736766e-17
1.633123935319537e+16

Never compare these results with ==. Use math.isclose(), and remember that its default is a relative tolerance, which is useless when the expected value is zero. Supply abs_tol for comparisons against zero:

print(math.isclose(math.sin(math.pi), 0.0))                  # relative tolerance only
print(math.isclose(math.sin(math.pi), 0.0, abs_tol=1e-12))   # what you want
False
True

For display, round at the very end (round(value, 10) or an f-string such as f"{value:.6f}") rather than rounding intermediate results. See the Python tutorial on floating-point arithmetic for the underlying reasons.

Vectorized trigonometry with NumPy

The math functions accept one scalar at a time. For arrays, use NumPy's universal functions, which apply element-wise and run in compiled code:

import numpy as np

deg = np.array([0, 30, 45, 90, 180])
rad = np.deg2rad(deg)                 # same as np.radians
print(np.round(np.sin(rad), 6))
print(np.round(np.cos(rad), 6))

x = np.array([1, -1, -1, 1])
y = np.array([1, 1, -1, -1])
print(np.rad2deg(np.arctan2(y, x)))
print(np.allclose(np.sin(np.pi), 0))
[0.       0.5      0.707107 1.       0.      ]
[ 1.        0.866025  0.707107  0.       -1.      ]
[  45.  135. -135.  -45.]
True

Naming differs from math: inverses are np.arcsin, np.arccos, np.arctan and np.arctan2 (NumPy 2.0 also added np.asin-style aliases for Array API compatibility). Error handling differs too: np.arcsin(2.0) returns nan with a RuntimeWarning instead of raising, so check results with np.isnan when inputs are untrusted. The full list is in the NumPy trigonometric functions reference.

Complex numbers with cmath

math.sin(1 + 2j) raises TypeError. The cmath module provides the same function names for complex arguments, and its inverses accept values outside [−1, 1] by returning complex results:

import cmath

print(cmath.sin(1 + 2j))
print(cmath.asin(2))          # math.asin(2) would raise ValueError
print(cmath.phase(-1 + 0j))   # angle of a complex number
print(cmath.polar(1 + 1j))    # (modulus, phase)
(3.165778513216168+1.9596010414216063j)
(1.5707963267948966+1.3169578969248166j)
3.141592653589793
(1.4142135623730951, 0.7853981633974483)

cmath.phase(z) is equivalent to math.atan2(z.imag, z.real). Details are in the cmath documentation.

Practical example: angle between two points and compass bearing

Two everyday tasks: the direction of a vector on a flat plane (screen coordinates, game objects) and the initial compass bearing between two GPS coordinates on a sphere.

import math

def angle_between(p, q):
    """Direction of p -> q in degrees, counter-clockwise from +x, in [0, 360)."""
    dx, dy = q[0] - p[0], q[1] - p[1]
    return math.degrees(math.atan2(dy, dx)) % 360

def initial_bearing(lat1, lon1, lat2, lon2):
    """Initial great-circle bearing in degrees, clockwise from north, in [0, 360)."""
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dlon = math.radians(lon2 - lon1)
    x = math.sin(dlon) * math.cos(phi2)
    y = math.cos(phi1) * math.sin(phi2) - math.sin(phi1) * math.cos(phi2) * math.cos(dlon)
    return math.degrees(math.atan2(x, y)) % 360

def haversine_km(lat1, lon1, lat2, lon2, r=6371.0):
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi = phi2 - phi1
    dlon = math.radians(lon2 - lon1)
    a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlon / 2) ** 2
    return 2 * r * math.asin(math.sqrt(a))

print(angle_between((0, 0), (3, 3)))
print(angle_between((0, 0), (-2, -2)))

paris, nyc = (48.8566, 2.3522), (40.7128, -74.0060)
print(round(initial_bearing(*paris, *nyc), 1))
print(round(haversine_km(*paris, *nyc)))
45.0
225.0
291.8
5837

Two details matter. Compass bearings are measured clockwise from north, so atan2 receives the east component as its first argument, the reverse of the mathematical convention. And on screens where the y-axis points down, negate dy if you want counter-clockwise angles to look "up". The haversine distance assumes a spherical Earth (mean radius 6371 km), which is accurate to roughly 0.5%; use a geodesy library for survey-grade work.

Common pitfalls

Summary

Use math for scalar trigonometry, always in radians, converting with math.radians and math.degrees. Know the principal ranges of asin, acos and atan, and prefer atan2(y, x) whenever you have two components. Treat results as approximations and compare with math.isclose plus abs_tol. Reach for NumPy (np.sin, np.deg2rad, np.arctan2) for arrays and cmath for complex values. The complete function list is in the math module documentation.