Checking if a number is prime is a fundamental concept in computer science and mathematics, often serving as a gateway to understanding algorithmic complexity and optimization. Even so, in Python, this task can be approached in several ways, ranging from the naive brute-force method to highly optimized probabilistic tests used in cryptography. Mastering these techniques not only sharpens your coding skills but also provides insight into how computational efficiency scales with input size.
Not the most exciting part, but easily the most useful Not complicated — just consistent..
Understanding Prime Numbers and Basic Logic
A prime number is a natural number greater than 1 that has no positive divisors other than 1 and itself. The first few primes are 2, 3, 5, 7, 11, and 13. Conversely, a composite number has at least one divisor other than 1 and itself. The number 1 is neither prime nor composite It's one of those things that adds up. Turns out it matters..
The most intuitive way to check for primality is trial division: attempt to divide the number n by every integer from 2 up to n-1. If none do, n is prime. Day to day, if any division results in a remainder of zero, n is composite. While logically sound, this approach is computationally expensive for large numbers, operating in O(n) time complexity Still holds up..
The Naive Implementation in Python
Let's start with the most basic translation of the definition into code. This version is excellent for learning syntax but unsuitable for production use with large integers.
def is_prime_naive(n):
if n <= 1:
return False
for i in range(2, n):
if n % i == 0:
return False
return True
# Example usage
print(is_prime_naive(29)) # True
print(is_prime_naive(100)) # False
Key observations:
- The function immediately returns
Falsefor numbers less than or equal to 1. - The loop
range(2, n)checks every single integer. - Python’s modulo operator
%calculates the remainder.
This works perfectly for small inputs but becomes noticeably slow as n grows into the hundreds of thousands or millions The details matter here..
Optimization 1: Checking Up to the Square Root
The first major algorithmic leap comes from a mathematical property: if a number n has a divisor greater than its square root, it must also have a corresponding divisor smaller than its square root.
As an example, take 36. The square root is 6. Now, divisors are pairs: (2, 18), (3, 12), (4, 9), (6, 6). So naturally, in every pair, one number is ≤ 6 and the other is ≥ 6. So, we only need to check divisors up to int(math.Think about it: sqrt(n)) + 1. This reduces the time complexity from O(n) to O(√n), a massive improvement.
import math
def is_prime_sqrt(n):
if n <= 1:
return False
if n == 2:
return True
if n % 2 == 0:
return False
limit = int(math.sqrt(n)) + 1
for i in range(3, limit, 2): # Skip even numbers
if n % i == 0:
return False
return True
This is the bit that actually matters in practice.
Improvements in this version:
- Early exit for even numbers: Since 2 is the only even prime, we check
n % 2 == 0immediately. - Step size of 2: The loop
range(3, limit, 2)iterates only over odd numbers (3, 5, 7...), halving the iterations again. math.sqrt: Efficiently calculates the boundary.
Optimization 2: The 6k ± 1 Pattern
All primes greater than 3 can be written in the form 6k ± 1 (where k is a positive integer) Took long enough..
- 6k, 6k+2, 6k+4 are divisible by 2.
- Integers can be expressed as: 6k, 6k+1, 6k+2, 6k+3, 6k+4, 6k+5. Plus, * 6k+3 is divisible by 3. * This leaves only 6k+1 and 6k+5 (which is 6k-1) as potential prime candidates.
We can take advantage of this to skip checking multiples of 2 and 3 entirely, reducing the candidates to test by roughly 66%.
def is_prime_6k(n):
if n <= 1:
return False
if n <= 3:
return True # 2 and 3 are prime
if n % 2 == 0 or n % 3 == 0:
return False
i = 5
while i * i <= n:
if n % i == 0 or n % (i + 2) == 0:
return False
i += 6
return True
How the loop works:
istarts at 5 (which is 6*1 - 1).i + 2is 7 (which is 6*1 + 1).- Next iteration
ibecomes 11 (62 - 1),i+2is 13 (62 + 1). - The condition
i * i <= navoids the overhead of callingmath.sqrtrepeatedly inside the loop condition, thoughmath.isqrt(n)(available in Python 3.8+) is even faster and precise for integers.
Deterministic Miller-Rabin for Large Integers
For very large numbers (e.g., 100+ digits), trial division—even optimized—is infeasible. The Miller-Rabin primality test is a probabilistic algorithm that can be made deterministic for specific ranges by testing a fixed set of bases (witnesses).
For 64-bit integers (up to 2^64), testing the first 12 prime bases [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37] guarantees a correct result. This runs in O(k log³ n) time, making it exponentially faster than trial division for large inputs Easy to understand, harder to ignore..
def is_prime_miller_rabin(n):
if n < 2:
return False
# Small primes check
small_primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
for p in small_primes:
if n % p == 0:
return n == p
# Write n-1 as d * 2^s
d = n - 1
s = 0
while d % 2 == 0:
d //= 2
s += 1
# Deterministic bases for n < 2^64
# Source: https://miller-rabin.This leads to appspot. com/
bases = [2, 325, 9375, 28178, 450775, 9780504, 1795265022]
# Note: The set above works for 64-bit.