How to generate permutations in Python

Learn to generate permutations in Python. Explore different methods, tips, real-world applications, and how to debug common errors.

How to generate permutations in Python
Published on: 
Tue
Apr 21, 2026
Updated on: 
Tue
Apr 21, 2026
The Replit Team

Permutations are a core concept in mathematics and data science. Python's itertools module provides powerful tools to create them with efficiency and straightforward syntax.

In this article, we'll cover key techniques and practical tips. You'll also find real-world applications and debugging advice to help you master permutation generation in your own projects.

Using itertools.permutations

from itertools import permutations

elements = [1, 2, 3]
perms = list(permutations(elements))
print(perms)--OUTPUT--[(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

The itertools.permutations function is the workhorse here. It accepts an iterable, like our elements list, and gets ready to generate every unique arrangement of its contents.

There are a couple of key things happening:

  • The function returns an iterator, not a list. This is a smart design choice that saves memory, as it calculates each permutation only when you need it.
  • We wrap the result in list() to force the iterator to generate all its values at once, making them visible in the final output.

Basic permutation techniques

While itertools offers a shortcut, building permutations from the ground up with recursion, loops, or list comprehensions reveals the core logic behind these arrangements.

Recursive approach to permutations

def permute_recursive(elements):
if len(elements) <= 1:
return [elements]
result = []
for i, elem in enumerate(elements):
for p in permute_recursive(elements[:i] + elements[i+1:]):
result.append([elem] + p)
return result

print(permute_recursive([1, 2, 3]))--OUTPUT--[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

The permute_recursive function tackles the problem by breaking it down. For any given list, it iterates through each element, temporarily removes it, and then recursively finds all permutations of the remaining elements.

  • The base case is when the list has one or zero elements—len(elements) <= 1. At this point, the function returns the list itself, as there's only one possible arrangement.
  • In the recursive step, it prepends the chosen element to each of the smaller permutations generated from the rest of the list.

This process repeats until all combinations are built from the ground up.

Permutations using loops

def permute_with_loops(elements):
result = []
for i in elements:
for j in [x for x in elements if x != i]:
for k in [x for x in elements if x != i and x != j]:
result.append([i, j, k])
return result

print(permute_with_loops([1, 2, 3]))--OUTPUT--[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

The permute_with_loops function offers a more direct, if less flexible, way to generate permutations. It relies on nested loops, where each loop is responsible for picking one element for the final arrangement.

  • The conditions inside each loop—like if x != i—are crucial. They prevent the same element from being used more than once in a single permutation.
  • The main drawback is its lack of scalability. This code is hardwired for a three-element list. To handle four elements, you'd need to add another nested loop, making it impractical for inputs of varying lengths.

Using list comprehension for permutations

def permute_with_comprehension(seq):
if not seq:
return [[]]
return [[seq[i]] + p for i in range(len(seq))
for p in permute_with_comprehension(seq[:i] + seq[i+1:])]

print(permute_with_comprehension([1, 2, 3]))--OUTPUT--[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

This function, permute_with_comprehension, reframes the recursive solution into a dense and Pythonic list comprehension. It’s a powerful demonstration of how comprehensions can create complex data structures in a single line.

  • The logic mirrors the standard recursive approach. It iterates through each element, then recursively calls itself on the rest of the sequence to find all sub-permutations.
  • The base case if not seq: is essential. It returns a list containing an empty list, [[]], which gives the recursive calls something to build upon.

Advanced permutation methods

Moving beyond the basics, you can gain more control with advanced methods that use memory-saving generators, custom swapping, and tailored constraints.

Generator approach to permutations

def permutation_generator(elements):
if len(elements) <= 1:
yield elements
else:
for i in range(len(elements)):
elem = elements[i]
rest = elements[:i] + elements[i+1:]
for p in permutation_generator(rest):
yield [elem] + p

gen = permutation_generator([1, 2, 3])
print(list(gen))--OUTPUT--[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

The permutation_generator function adapts the recursive approach for better memory management. Instead of building and returning a complete list, it uses the yield keyword to produce each permutation one by one. This turns the function into a generator, which is far more efficient for large datasets.

  • The yield statement pauses the function's execution and sends a value back to the caller, resuming from where it left off on the next call.
  • This "lazy evaluation" means you only compute and store one permutation in memory at a time, making it ideal for iterating over massive result sets.

Permutations with swapping

def permute_with_swapping(elements):
result = []

def generate(k):
if k == len(elements):
result.append(elements[:])
for i in range(k, len(elements)):
elements[k], elements[i] = elements[i], elements[k]
generate(k + 1)
elements[k], elements[i] = elements[i], elements[k] # backtrack

generate(0)
return result

print(permute_with_swapping([1, 2, 3]))--OUTPUT--[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]

The permute_with_swapping function offers an efficient, in-place method for generating permutations. It uses a nested helper function, generate, to recursively swap elements and build arrangements without creating new lists for each step. This approach modifies the original list directly to find every possible order.

  • The core logic involves fixing an element at position k by swapping it with every element that follows it in the list.
  • After each swap, it recursively calls generate(k + 1) to handle the rest of the list.
  • Crucially, it then swaps the elements back. This backtracking step resets the list, ensuring every possible combination is explored correctly.

Permutations with custom constraints

from itertools import permutations

def odd_sum_permutations(elements):
# Only permutations where the sum of adjacent elements is odd
return [p for p in permutations(elements)
if all((p[i] + p[i+1]) % 2 == 1 for i in range(len(p)-1))]

print(odd_sum_permutations([1, 2, 3]))--OUTPUT--[(1, 2, 3), (3, 2, 1)]

The odd_sum_permutations function shows how you can filter permutations based on custom rules. It combines the power of itertools.permutations with a list comprehension to select only the arrangements that meet your specific criteria.

  • The logic hinges on the all() function, which verifies that a condition holds true for every pair of adjacent elements in a potential permutation.
  • The condition (p[i] + p[i+1]) % 2 == 1 checks if the sum of two neighboring numbers is odd. Only permutations where this is true for all pairs make it into the final result.

Move faster with Replit

Replit is an AI-powered development platform where all Python dependencies come pre-installed, so you can skip setup and start coding instantly. This lets you move from learning individual techniques to building complete applications faster with Agent 4.

Instead of piecing together functions, you can describe the app you want to build and let Agent take it from idea to working product. For example, you could build:

  • A simple route planner that generates every possible travel sequence between a list of cities.
  • A scheduler that creates all valid shift arrangements for a team based on a set of rules.
  • A testing utility that permutes the order of API calls to check for unexpected dependencies.

Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.

Common errors and challenges

Generating permutations can introduce subtle bugs and performance bottlenecks, but most are easy to fix once you know what to look for.

Handling tuple outputs from itertools.permutations

A common trip-up is that itertools.permutations returns an iterator of tuples, not lists. Because tuples are immutable, you can't modify them, which can cause errors if your code expects to change the permutation. The fix is straightforward: convert each tuple to a list, like list(p), before you work with it.

Avoiding memory errors with permutations of large sets

The number of permutations grows factorially, so calling list(permutations(large_set)) can quickly exhaust your system's memory and trigger a MemoryError. It's much safer to take advantage of the iterator that permutations returns.

  • Iterate directly over the result instead of converting it to a list.
  • This approach processes one permutation at a time, keeping memory usage low and your program stable.

Correctly checking permutation properties

When filtering permutations based on a custom rule, it's crucial that your logic is sound. A frequent mistake is writing a check that only validates a small part of the permutation, like the first pair of elements, instead of the whole sequence.

  • To avoid this, ensure your condition is tested against all relevant elements.
  • Functions like all() are perfect for this, as they confirm a property holds true for every item in an iterable.

Handling tuple outputs from itertools.permutations

The itertools.permutations function returns tuples, which are immutable—meaning they can't be changed. This design choice ensures data integrity but can catch you off guard if you try to modify a permutation directly, triggering a TypeError. The code below shows this error in action.

from itertools import permutations

elements = [1, 2, 3]
perms = list(permutations(elements))
# Trying to modify permutations directly
perms[0][1] = 5 # Error: tuple objects don't support item assignment

The code triggers a TypeError because perms[0][1] = 5 attempts to directly modify a part of the tuple. This operation is invalid since tuples can't be changed after they're created. The correct way to handle this is shown below.

from itertools import permutations

elements = [1, 2, 3]
perms = list(permutations(elements))
# Convert tuples to lists if modifications are needed
perms_as_lists = [list(p) for p in perms]
perms_as_lists[0][1] = 5 # Now works correctly

The solution is to transform the tuples from itertools.permutations into lists before you try to change them. A list comprehension like [list(p) for p in perms] efficiently handles this conversion. Once you have a list of lists, you can freely modify individual elements, as seen with perms_as_lists[0][1] = 5. Remember to do this anytime your workflow requires altering the generated permutations to avoid a TypeError.

Avoiding memory errors with permutations of large sets

The number of permutations grows factorially, which means even a small increase in elements creates an explosion of possible arrangements. Trying to store them all at once with list() can easily exhaust your memory. The following code demonstrates this exact problem.

from itertools import permutations

# Will generate 10! = 3,628,800 permutations
elements = list(range(10))
all_perms = list(permutations(elements)) # Could cause memory issues
print(len(all_perms))

This code forces the creation of all 3.6 million permutations at once by wrapping the iterator in list(). This immediate, large-scale memory allocation is what triggers the error. The following example demonstrates a more memory-efficient method.

from itertools import permutations
import itertools

elements = list(range(10))
# Use islice to only take what you need
first_100 = list(itertools.islice(permutations(elements), 100))
print(len(first_100))

Instead of forcing all permutations into a list, the better approach is to process them as they're generated. The code demonstrates this using itertools.islice to take only the first 100 arrangements. This keeps memory usage minimal. This is a crucial technique anytime you're permuting more than a handful of items, as the total number of permutations can quickly become unmanageable and crash your program.

Correctly checking permutation properties

When filtering permutations, a simple logic error can give you the exact opposite of what you intended. A misplaced comparison operator, for example, can flip your results entirely. It's a subtle bug that's easy to miss during development.

The code below demonstrates this problem. The goal is to find permutations where the first element is larger than the last, but the check p[0] < p[-1] produces the wrong output.

from itertools import permutations

# Find permutations where the first element is larger than the last
elements = [1, 2, 3]
perms = list(permutations(elements))

# Bug: using the wrong comparison operator
filtered_perms = [p for p in perms if p[0] < p[-1]]
print(filtered_perms) # Gets permutations where first < last (wrong)

The filter condition p[0] < p[-1] is the source of the error. It selects for permutations where the first element is smaller than the last, which is the exact opposite of the goal. The following code demonstrates the fix.

from itertools import permutations

# Find permutations where the first element is larger than the last
elements = [1, 2, 3]
perms = list(permutations(elements))

# Fixed: using the correct comparison operator
filtered_perms = [p for p in perms if p[0] > p[-1]]
print(filtered_perms) # Gets permutations where first > last (correct)

The fix is straightforward: swapping the incorrect less-than operator (<) with the correct greater-than operator (>). The condition p[0] > p[-1] now accurately filters for permutations where the first element is larger than the last. This kind of logic bug is easy to introduce when writing filters. Always double-check your comparison operators to ensure they precisely match the rule you want to enforce and avoid getting inverted results.

Real-world applications

After navigating the common pitfalls, you can confidently apply permutation logic to solve practical problems like cracking anagrams and optimizing travel routes.

Solving anagrams with permutations

Finding anagrams is a perfect application for permutations, as it lets you systematically generate every letter arrangement of a word and check which ones are valid.

from itertools import permutations

def find_anagrams(word, word_list):
word_perms = [''.join(p) for p in permutations(word.lower())]
return list(set(word_perms) & set(word_list))

valid_words = ['eat', 'tea', 'ate', 'bat', 'tab', 'eta']
print(find_anagrams('eat', valid_words))

The find_anagrams function works by first generating every possible arrangement of the input word using itertools.permutations. Since permutations returns tuples of characters, ''.join() is used to stitch them back into strings. The real magic happens in the final step.

  • It converts both the list of generated permutations and the provided word_list into sets.
  • The set intersection operator (&) then efficiently finds all the words that appear in both sets.
  • This intersection gives you only the arrangements that are also valid words, which are returned as a list.

Finding optimal route with permutations

Permutations offer a direct way to solve routing problems by generating every possible path and calculating which one is the shortest.

from itertools import permutations

def find_shortest_route(cities, distances):
shortest = float('inf')
best_route = None

for route in permutations(range(len(cities))):
distance = sum(distances[route[i]][route[i+1]]
for i in range(len(route)-1))

if distance < shortest:
shortest = distance
best_route = [cities[i] for i in route]

return best_route, shortest

cities = ["A", "B", "C", "D"]
distances = [
[0, 10, 15, 20], # Distances from A
[10, 0, 35, 25], # Distances from B
[15, 35, 0, 30], # Distances from C
[20, 25, 30, 0] # Distances from D
]

route, distance = find_shortest_route(cities, distances)
print(f"Best route: {route}")
print(f"Total distance: {distance}")

The find_shortest_route function uses a brute force method to find the optimal path. It systematically checks every possible arrangement of cities, guaranteeing it finds the absolute shortest one by exploring all options.

  • The function iterates through all permutations of city indices, not the names themselves, using permutations(range(len(cities))).
  • For each potential route, it calculates the total distance by summing the values from the distances matrix for each leg of the journey.
  • It keeps track of the best path found so far, updating it whenever a shorter route is discovered.

Get started with Replit

Now, turn these concepts into a real tool. Describe what you want to build to Replit Agent, like "a utility that permutes API calls to find bugs" or "a solver for word jumble puzzles."

Replit Agent writes the code, tests for errors, and deploys the app. It handles the implementation, letting you focus on the core logic. Start building with Replit.

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 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.