Trigonometric Functions in Python: A Complete Guide

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:
| Function | Valid input | Output 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
- Degrees passed as radians. If results look random, check for a missing
math.radians(). - Swapped atan2 arguments. It is
atan2(y, x); swapping them reflects the angle across the 45° line. - Exact comparisons. Use
math.isclose(..., abs_tol=...)near zero,np.allclosefor arrays. - Domain errors from rounding. Clamp inputs to
asin/acosinto [−1, 1]. - Calling math functions on arrays.
math.sin(np_array)fails for arrays with more than one element; usenp.sin. - Expecting tan(π/2) to be infinite. It returns a huge finite number; guard divisions explicitly if you rely on vertical angles.
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.
PythonFactorial in Python
Compute factorials in Python with a loop, recursion, math.factorial, reduce and memoization, with input validation, big integers and real timeit benchmarks.
PythonRandom integers
Generate random integers in Python with random.randint, randrange, sample, secrets.randbelow and NumPy's rng.integers, plus seeding, uniqueness and speed.
DataKurtosis in Python
Compute kurtosis in Python with scipy.stats.kurtosis, pandas Series.kurt and NumPy. Excess vs Pearson, the bias flag, worked examples and how to interpret it.