Remove Duplicates from a List with Python Set
python set remove duplicates: Learn how to use Python sets to remove duplicates from lists, handle order preservation, and understand performance tradeoffs.
When you need to remove duplicates from a list in Python, the set type is often the first tool that comes to mind. The phrase python set remove duplicates describes a common operation: converting a list to a set to drop repeated values, then converting back to a list. This works because a set stores only unique elements, and the conversion naturally filters out duplicates.
The basic pattern is straightforward:
original_list = [3, 1, 2, 3, 4, 2, 5] unique_list = list(set(original_list)) print(unique_list) # Output order may vary
The set constructor iterates over the list, hashes each element, and stores only one copy of each distinct value. The resulting list contains the unique elements, but the order is not guaranteed to match the original list. For many use cases this is acceptable, but if the relative order matters, you need a different approach.
What Happens to Order When You Use a Set
A set is an unordered collection. The internal hash table determines the iteration order based on the hash values of the elements and the current table size. This means that list(set(original_list)) can produce a list in an order that appears arbitrary and may change between Python versions, or even between runs if hash randomization is enabled (which is the default for strings).
For example:
words = ["apple", "banana", "apple", "cherry"] unique_words = list(set(words)) print(unique_words) # e.g., ['banana', 'cherry', 'apple']
The order of unique_words is not deterministic from the source list. If you rely on the original order, this behavior can introduce subtle bugs, especially in tests or when the output feeds into a user-facing feature.
Preserving Order with dict.fromkeys or a Loop
When order preservation is required, you can use dict.fromkeys() to remove duplicates while keeping the first occurrence of each element. This works because dictionaries in Python 3.7+ preserve insertion order, and dict.fromkeys() creates a dictionary with the list elements as keys and None as values. Duplicate keys are automatically dropped, and the keys retain their original order.
original_list = [3, 1, 2, 3, 4, 2, 5] unique_list = list(dict.fromkeys(original_list)) print(unique_list) # [3, 1, 2, 4, 5]
This approach is clean and readable. It also works with any hashable elements, just like a set. If you are on Python 3.6 or earlier, dictionary order is not guaranteed, but most modern environments use 3.7+.
Alternatively, you can build the result manually with a loop and a set for membership checks:
def unique_preserve_order(items): seen = set() result = [] for item in items: if item not in seen: seen.add(item) result.append(item) return result
This manual loop gives you full control and works on any iterable, not just lists. It also makes it easy to add custom logic, such as skipping None values or applying a normalization function before deduplication.
Handling Unhashable Elements
Sets and dictionary keys require elements to be hashable. Lists, dictionaries, and other mutable collections are not hashable, so list(set(nested_list)) raises a TypeError if the elements are lists or dicts.
nested = [[1, 2], [3, 4], [1, 2]] # list(set(nested)) # TypeError: unhashable type: 'list'
For unhashable elements, you need a different strategy. One common approach is to convert each element to a hashable representation, such as a tuple for lists, or a frozenset for sets. For example:
nested = [[1, 2], [3, 4], [1, 2]] unique_nested = list(set(tuple(item) for item in nested)) # Result: [(1, 2), (3, 4)]
This changes the element type, so you may need to convert back to the original type afterwards. For dictionaries, you can use json.dumps() or a custom serialization, but be aware of key ordering and value types.
If the elements are complex objects, you can implement __hash__ and __eq__ on the class, but that is often overkill for a simple deduplication task. In those cases, a manual loop with a custom equality check might be more appropriate.
Performance and Memory Tradeoffs of Set-Based Deduplication
The main advantage of using a set for deduplication is speed. Building a set and then converting back to a list runs in O(n) average time, where n is the number of elements in the original list. The hash table lookups are constant-time on average, so the whole operation scales linearly with input size.
In contrast, a naive approach that checks each element against the result list using in would be O(n²) in the worst case, because each membership check scans the growing result list. For large lists, the set-based method is dramatically faster.
Memory usage is also a consideration. A set stores a hash table, which typically uses more memory than a list of the same length. When you create set(original_list), the set holds all unique elements until the operation completes. If the original list is huge and contains many duplicates, the set may still be large, but it will be smaller than the original list. However, if the list contains almost all unique values, the set will be roughly the same size as the list, and the temporary memory overhead is significant.
The manual loop with a seen set has the same memory profile: it stores a set of all unique elements plus the result list. The dict.fromkeys() approach stores a dictionary, which has similar memory overhead to a set but also holds None values, so it is slightly heavier.
For most applications, the performance gain outweighs the memory cost. But if you are processing very large data streams and memory is tight, you might consider an external sorting or a database operation instead of loading everything into a set.
When to Use Set vs Other Deduplication Approaches
Choosing the right deduplication method depends on your specific requirements:
- Use
list(set(items))when order does not matter and all elements are hashable. This is the simplest and fastest option. - Use
list(dict.fromkeys(items))when you need to preserve the first occurrence order and the elements are hashable. This is concise and works well for most real-world lists. - Use a manual loop when you need custom logic, such as case-insensitive deduplication, or when elements are unhashable and you need to control the comparison.
- Use
pandasornumpyif you are already working with tabular data and need to deduplicate rows based on specific columns. These libraries offer vectorized operations that are more efficient for large datasets.
Here is a quick comparison of the common methods:
| Method | Order Preserved | Hashable Required | Time Complexity | Memory Overhead |
|---|---|---|---|---|
list(set(items)) | No | Yes | O(n) | Set table |
list(dict.fromkeys(...)) | Yes (3.7+) | Yes | O(n) | Dict table |
| Manual loop with seen set | Yes | Yes | O(n) | Set + result |
| Manual loop without set | Yes | No | O(n²) | Result only |
The manual loop without a set is rarely a good choice for large lists because of the quadratic time cost. It is only reasonable for very small lists or when elements are not hashable and you cannot easily convert them.
A Practical Example: Deduplicating User Input
Consider a scenario where you collect a list of email addresses from a form, and users may submit duplicates. You want to remove duplicates while keeping the order of first submission. Using dict.fromkeys() is ideal:
emails = ["alice@example.com", "bob@example.com", "alice@example.com", "carol@example.com"] unique_emails = list(dict.fromkeys(emails))
This preserves the order in which the emails were first entered, which is important for audit trails or when the order corresponds to submission time. If you used set(), the order would be unpredictable, and you might lose the chronological sequence.
For case-insensitive deduplication, you can normalize the email addresses to lowercase before checking:
def unique_case_insensitive(items): seen = set() result = [] for item in items: key = item.lower() if key not in seen: seen.add(key) result.append(item) return result
This manual loop gives you full control over what constitutes a duplicate, which is impossible with a plain set or dictionary approach unless you pre‑normalize the data.
Compatibility and Version Considerations
The behavior of dict.fromkeys() preserving order depends on the Python version. Python 3.7 officially guarantees that dictionaries maintain insertion order, but this was already the case in CPython 3.6 as an implementation detail. If you are supporting Python 3.5 or earlier, you cannot rely on dictionary order, so you should use the manual loop instead.
Similarly, set iteration order has never been guaranteed, and it can change across Python versions. Code that depends on the order of list(set(...)) is fragile and may break when the runtime environment changes. Always assume that set order is arbitrary.
For code that must run on multiple Python versions, the manual loop with a seen set is the safest option because it does not depend on dictionary or set ordering guarantees. It also works with any iterable, not just lists, making it a versatile utility function.
When you are working with very large datasets, consider whether you need to load all data into memory at once. A set or dictionary holds all unique elements, which can be expensive. If the data is too large, you might process it in chunks or use an external tool like sort -u in a shell pipeline, but that is outside the scope of Python's standard library.
Ultimately, the set-based approach is a powerful tool for deduplication, but it is not a one-size-fits-all solution. Understanding the order implications, hashability constraints, and memory tradeoffs will help you choose the right method for your specific use case.