How to write an algorithm in Python

Learn to write algorithms in Python with our guide. Discover different methods, tips, real-world applications, and how to debug errors.

How to write an algorithm in Python
Published on: 
Fri
Feb 13, 2026
Updated on: 
Mon
Feb 16, 2026
The Replit Team Logo Image
The Replit Team

To write an algorithm in Python is a core skill for developers. It allows you to solve complex problems with code that is efficient, structured, and reusable.

In this article, you'll explore fundamental techniques and practical tips for algorithm design. You will also review real-world applications and learn effective advice to debug your code, which helps you build powerful solutions.

Creating a simple algorithm with a function

def calculate_average(numbers):
total = sum(numbers)
return total / len(numbers)

result = calculate_average([10, 20, 30, 40, 50])
print(f"The average is: {result}")--OUTPUT--The average is: 30.0

The calculate_average function demonstrates a key principle of algorithm design—encapsulation. By wrapping the logic in a function, you create a reusable and self-contained block of code that solves a specific problem. This approach keeps your main program clean and makes the algorithm easier to test and debug independently.

  • It leverages Python's built-in sum() function for an efficient calculation of the total.
  • The result is then divided by the number of items, found using len().

Basic algorithm design patterns

Building on the calculate_average function, you can create more robust algorithms by incorporating control structures, input validation, and modular helper functions.

Using control structures in algorithms

def find_maximum(numbers):
if not numbers:
return None
max_value = numbers[0]
for num in numbers:
if num > max_value:
max_value = num
return max_value

print(find_maximum([5, 12, 9, 42, 17]))--OUTPUT--42

The find_maximum function demonstrates how control structures direct an algorithm's flow. It first validates the input by checking if not numbers, a simple yet powerful way to prevent errors with empty lists. This makes your algorithm more robust.

  • A for loop iterates through each number, allowing the algorithm to examine every element.
  • Inside the loop, an if statement continuously compares the current number against the stored max_value, updating it whenever a larger number is found.

Adding input validation to your calculate_discount() algorithm

def calculate_discount(price, discount_percent):
if not isinstance(price, (int, float)) or price < 0:
raise ValueError("Price must be a positive number")
if not 0 <= discount_percent <= 100:
raise ValueError("Discount must be between 0 and 100")

return price * (1 - discount_percent / 100)

print(calculate_discount(100, 20))--OUTPUT--80

The calculate_discount function introduces robust input validation. Before performing any calculations, it ensures the data it receives is sensible. This practice prevents unexpected errors and makes your algorithm more reliable.

  • It uses isinstance() to confirm the price is a number and checks that it isn't negative.
  • It also verifies that the discount_percent falls within a logical range of 0 to 100.
  • If either condition fails, the function raises a ValueError, immediately stopping execution and clearly signaling what went wrong.

Creating modular algorithms with helper functions

def is_prime(n):
if n <= 1:
return False
for i in range(2, int(n**0.5) + 1):
if n % i == 0:
return False
return True

def get_primes_below(limit):
return [num for num in range(2, limit) if is_prime(num)]

print(get_primes_below(20))--OUTPUT--[2, 3, 5, 7, 11, 13, 17, 19]

This example showcases modular design by breaking a problem into smaller, manageable pieces. The main function, get_primes_below, delegates the complex task of checking for prime numbers to a dedicated helper function, is_prime. This separation makes your code cleaner and easier to debug because each function has a single responsibility.

  • The is_prime function efficiently checks for primality by testing divisibility using the modulo operator (%) only up to the number's square root (n**0.5).
  • get_primes_below then uses a list comprehension to call is_prime for each number up to its limit, building the final list.

Advanced algorithm techniques

With the fundamentals covered, you can now write more sophisticated algorithms using advanced techniques like binary_search(), decorators, and memoization for greater efficiency.

Implementing binary_search() with optimized time complexity

def binary_search(sorted_list, target):
left, right = 0, len(sorted_list) - 1

while left <= right:
mid = (left + right) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
left = mid + 1
else:
right = mid - 1

return -1 # Not found

print(binary_search([1, 3, 5, 7, 9, 11], 5))--OUTPUT--2

The binary_search function is a highly efficient "divide and conquer" algorithm for finding an item in a sorted list. Instead of checking each element sequentially, it repeatedly halves the search space, dramatically reducing lookup time.

  • It compares the target to the middle element of the list.
  • If the middle element is too high, it discards the upper half; if it's too low, it discards the lower half.
  • This process continues until the target is found or the function returns -1, indicating the item isn't present.

Using decorators to enhance algorithm functionality

import time

def timing_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"Function {func.__name__} took {end_time - start_time:.6f} seconds")
return result
return wrapper

@timing_decorator
def sort_algorithm(arr):
return sorted(arr)

sort_algorithm([5, 2, 9, 1, 5, 6])--OUTPUT--Function sort_algorithm took 0.000058 seconds
[1, 2, 5, 5, 6, 9]

Decorators offer a clean way to enhance a function's behavior without changing its code. Here, the @timing_decorator wraps the sort_algorithm function to measure its performance. It’s a practical method for benchmarking different algorithms against each other.

  • The decorator's wrapper function captures the start time, runs the original function with its arguments (*args, **kwargs), and then captures the end time.
  • Finally, it prints the total execution time, providing instant feedback on your algorithm's efficiency.

Writing efficient recursive algorithms with memoization

def fibonacci(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
return memo[n]

for i in range(10):
print(fibonacci(i), end=" ")--OUTPUT--0 1 1 2 3 5 8 13 21 34

The fibonacci function demonstrates memoization, a powerful optimization for recursive algorithms. A standard recursive approach to Fibonacci is inefficient because it re-computes the same values over and over. This version cleverly avoids that redundant work.

  • It uses a dictionary, memo, as a cache to store results it has already calculated.
  • Before doing any work, the function first checks if n in memo. If the value exists, it's returned immediately.
  • Otherwise, it computes the result, saves it to memo, and then returns it, ensuring each Fibonacci number is calculated only once.

Move faster with Replit

Replit is an AI-powered development platform that transforms natural language into working applications. Describe what you want to build, and Replit Agent creates it—complete with databases, APIs, and deployment.

For the algorithm techniques you've just reviewed, Replit Agent can turn them into production-ready tools. You can use it to build applications that leverage the logic from this article.

  • Build an e-commerce discount calculator that validates prices and percentages, based on the calculate_discount() function.
  • Create an inventory search tool that uses binary_search() to instantly find items in a large, sorted database.
  • Deploy a utility that generates prime numbers for security applications, expanding on the get_primes_below() logic.

Describe your app idea, and Replit Agent writes the code, tests it, and fixes issues automatically, all in your browser. Try Replit Agent to bring your concepts to life.

Common errors and challenges

Writing robust algorithms means anticipating common pitfalls, from handling unexpected inputs to managing recursion and default arguments.

Handling empty lists in the calculate_average() function

The calculate_average() function works perfectly with a list of numbers, but it breaks if you give it an empty list. When that happens, the code tries to divide by zero, which raises a ZeroDivisionError and crashes the program. This is a classic edge case that’s easy to miss.

  • To fix this, you can add a simple check at the start of the function. An if not numbers: statement will catch an empty list, allowing you to return a safe value like 0 or None and make your algorithm more resilient.

Preventing infinite recursion in the factorial() function

Recursive functions are powerful, but they come with a major risk: infinite recursion. If a function like factorial() lacks a proper "base case"—a condition that tells it when to stop—it will call itself endlessly. This quickly consumes all available memory and results in a RecursionError.

  • You can prevent this by making sure your function has a clear exit strategy. For a factorial function, a condition like if n == 0: return 1 serves as the base case that stops the recursion and ensures the algorithm finishes correctly.

Avoiding bugs with mutable default arguments in the append_to_list() function

One of Python's trickiest gotchas is using a mutable object, like a list or dictionary, as a default function argument. For a function defined as def append_to_list(item, my_list=[]), the empty list [] is created only once when the function is first defined, not every time it's called.

  • This means every call to append_to_list() without a second argument modifies the exact same list, leading to unexpected behavior. The best practice is to use None as the default and then create a new list inside the function with a check like if my_list is None: my_list = [].

Handling empty lists in the calculate_average() function

While the calculate_average() function handles populated lists perfectly, it fails when given an empty one. This is a classic edge case. The code attempts to divide by zero, triggering a ZeroDivisionError that crashes the program. The example below demonstrates this issue.

def calculate_average(numbers):
total = sum(numbers)
return total / len(numbers)

print(calculate_average([]))

Since len([]) evaluates to 0, the expression total / len(numbers) fails because division by zero is mathematically undefined. The corrected code below shows how to prevent this error from crashing your program.

def calculate_average(numbers):
if not numbers:
return 0
total = sum(numbers)
return total / len(numbers)

print(calculate_average([]))

The corrected calculate_average function adds a simple guard clause. By checking if not numbers: at the beginning, you catch empty lists before they cause a ZeroDivisionError. This makes the algorithm more robust by returning a default value of 0 instead of crashing.

  • This is a crucial check whenever your algorithm performs division or accesses elements based on a collection's length, preventing unexpected errors with empty inputs.

Preventing infinite recursion in the factorial() function

A recursive function like factorial() calculates a result by calling itself with a smaller value. But without a base case to tell it when to stop, it will call itself endlessly. This leads to a RecursionError. The following code shows this error in action.

def factorial(n):
return n * factorial(n-1)

print(factorial(5))

This version of factorial() keeps calling itself with n-1, but there's nothing to stop it. The recursion descends into negative numbers indefinitely, which triggers the error. The corrected code below shows how to properly terminate the function.

def factorial(n):
if n <= 1:
return 1
return n * factorial(n-1)

print(factorial(5))

The corrected factorial() function adds a "base case" with if n <= 1: return 1. This condition is the secret to stopping the recursion. Without it, the function would call itself forever and crash with a RecursionError.

  • This check acts as an exit strategy, guaranteeing the chain of calls eventually ends and returns a final value.
  • You should always define a clear base case in any recursive algorithm to prevent it from running out of control.

Avoiding bugs with mutable default arguments in the append_to_list() function

A mutable default argument, like a list in def append_to_list(item, my_list=[]), creates a subtle but significant bug. Because the list is created only once, it persists between calls, leading to accumulated results you didn't intend. The code below illustrates this problem.

def append_to_list(item, my_list=[]):
my_list.append(item)
return my_list

print(append_to_list(1))
print(append_to_list(2))

The second call to append_to_list() doesn't start with a fresh list. Instead, it modifies the list from the first call, which is why the output is cumulative. The corrected implementation below avoids this shared state.

def append_to_list(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list

print(append_to_list(1))
print(append_to_list(2))

The corrected append_to_list function solves the shared state bug by setting the default argument to None instead of an empty list. This pattern ensures that a new list is created inside the function every time it's called without a specific list provided, preventing data from one call from leaking into the next.

  • The key is the if my_list is None: check, which guarantees a fresh start. You should always use this approach when a function needs a mutable default argument.

Real-world applications

Now that you've learned to build and debug algorithms, you can apply them to solve practical problems in text analysis and data processing.

Using algorithms for text analysis with count_word_frequency()

The count_word_frequency() function offers a straightforward way to analyze text by breaking it down and tallying how often each word appears.

def count_word_frequency(text):
words = text.lower().split()
frequency = {}
for word in words:
frequency[word] = frequency.get(word, 0) + 1
return frequency

sample_text = "The quick brown fox jumps over the lazy dog"
print(count_word_frequency(sample_text))

This function tallies word occurrences by first normalizing the input with text.lower().split(). This step ensures that words like "The" and "the" are counted together, creating a clean list to work with.

  • The clever part is the expression frequency.get(word, 0) + 1.
  • This single line handles both existing and new words. The get() method safely fetches a word's current count, defaulting to 0 if the word hasn't been seen yet. This approach avoids a KeyError and keeps the code compact.

Implementing a filter_and_analyze() function for data processing

The filter_and_analyze() function is a practical tool for processing datasets, allowing you to isolate relevant information and compute summary statistics in one go. It uses a concise list comprehension—[x for x in data if x > threshold]—to efficiently create a new list containing only the values that meet your criteria.

Once the data is filtered, the function returns a dictionary that summarizes the results:

  • count: The number of data points that passed the filter.
  • average: The mean of the filtered values.
  • max: The highest value in the filtered set.

The function also includes a crucial check for an empty list. If no data points meet the threshold, it returns safe default values like None, which prevents your program from crashing with an error when trying to perform calculations on an empty set.

def filter_and_analyze(data, threshold):
if not isinstance(threshold, (int, float)):
raise ValueError("Threshold must be a number")

filtered_data = [x for x in data if x > threshold]

if not filtered_data:
return {"count": 0, "average": None, "max": None}

return {
"count": len(filtered_data),
"average": sum(filtered_data) / len(filtered_data),
"max": max(filtered_data)
}

sensor_readings = [23.1, 19.8, 31.4, 18.9, 27.5, 22.2]
print(filter_and_analyze(sensor_readings, 25))

The filter_and_analyze function demonstrates robust design by first validating its inputs. It checks if the threshold is a number using isinstance() and raises a ValueError if the check fails, which prevents unexpected behavior. This makes your algorithm more predictable.

After filtering the data, the function bundles multiple pieces of information into a single dictionary. This is a powerful pattern in Python for returning structured data, as each value is clearly labeled with a key like 'count' or 'average'.

Get started with Replit

Turn your new algorithm skills into a real tool. Describe what you want to build, like "a web app that analyzes text for word frequency" or "a tool to filter sensor data and find the max value".

Replit Agent writes the code, tests for errors, and deploys your app. Start building with Replit to see your algorithms power a live application.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started for free

Create & deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.