Python - List Comprehension vs Map/Filter

List comprehensions and map/filter serve the same purpose but with measurably different performance characteristics. Here's a direct comparison using Python's timeit module:

Key Insights

  • List comprehensions are 10-20% faster than map/filter for simple transformations due to reduced function call overhead and optimized bytecode generation
  • Map and filter excel in functional composition pipelines and when working with existing named functions, while list comprehensions offer superior readability for complex conditional logic
  • Memory efficiency differs significantly: generators (with parentheses) provide lazy evaluation for both approaches, while lists materialize immediately—critical for large datasets

Performance Benchmarks: The Real Numbers

List comprehensions and map/filter serve the same purpose but with measurably different performance characteristics. Here’s a direct comparison using Python’s timeit module:

import timeit
from functools import reduce

# Dataset
numbers = list(range(1000000))

# List comprehension approach
def list_comp_transform():
    return [x * 2 for x in numbers if x % 2 == 0]

# Map/filter approach
def map_filter_transform():
    return list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, numbers)))

# Benchmark
lc_time = timeit.timeit(list_comp_transform, number=100)
mf_time = timeit.timeit(map_filter_transform, number=100)

print(f"List comprehension: {lc_time:.4f}s")
print(f"Map/filter: {mf_time:.4f}s")
print(f"Difference: {((mf_time - lc_time) / lc_time * 100):.2f}%")

On typical hardware, list comprehensions complete 15-20% faster. The difference stems from Python’s bytecode optimization—list comprehensions compile to specialized LOAD_FAST opcodes, while map/filter incur additional function call overhead.

Readability: Complex Conditions

List comprehensions dominate when combining multiple conditions or transformations:

# Extract and transform nested data
users = [
    {"name": "Alice", "age": 28, "active": True, "purchases": [100, 200]},
    {"name": "Bob", "age": 35, "active": False, "purchases": [50]},
    {"name": "Charlie", "age": 42, "active": True, "purchases": [300, 150, 75]}
]

# List comprehension: readable single expression
active_high_spenders = [
    user["name"] 
    for user in users 
    if user["active"] 
    if sum(user["purchases"]) > 200
]

# Map/filter equivalent: nested and harder to parse
active_high_spenders_mf = list(map(
    lambda u: u["name"],
    filter(
        lambda u: sum(u["purchases"]) > 200,
        filter(lambda u: u["active"], users)
    )
))

The list comprehension reads left-to-right with clear intent. The map/filter version requires inside-out parsing and multiple lambda definitions.

Functional Composition: Where Map/Filter Shine

Map and filter integrate seamlessly into functional programming patterns, especially with existing functions:

from functools import partial
import re

# Named transformation functions
def sanitize_email(email):
    return email.strip().lower()

def is_valid_email(email):
    return bool(re.match(r'^[\w\.-]+@[\w\.-]+\.\w+$', email))

def extract_domain(email):
    return email.split('@')[1]

# Functional pipeline
emails = ["  Alice@EXAMPLE.com", "invalid.email", "bob@test.org  ", "charlie@demo.com"]

# Map/filter: compose with existing functions
pipeline = map(extract_domain, 
               filter(is_valid_email, 
                      map(sanitize_email, emails)))
valid_domains = list(pipeline)

# List comprehension equivalent: less composable
valid_domains_lc = [
    extract_domain(sanitize_email(email))
    for email in emails
    if is_valid_email(sanitize_email(email))
]

Notice the list comprehension calls sanitize_email twice—inefficient and error-prone. With map/filter, each function applies once in sequence.

Memory Efficiency: Lazy vs Eager Evaluation

Both approaches support lazy evaluation through generators, but syntax differs:

import sys

# Large dataset simulation
def generate_data():
    return range(10000000)

# Eager evaluation: materializes entire list
eager_lc = [x * 2 for x in generate_data() if x % 3 == 0]
eager_mf = list(map(lambda x: x * 2, filter(lambda x: x % 3 == 0, generate_data())))

print(f"Eager LC size: {sys.getsizeof(eager_lc) / 1024 / 1024:.2f} MB")

# Lazy evaluation: generator expressions
lazy_lc = (x * 2 for x in generate_data() if x % 3 == 0)
lazy_mf = map(lambda x: x * 2, filter(lambda x: x % 3 == 0, generate_data()))

print(f"Lazy LC size: {sys.getsizeof(lazy_lc)} bytes")
print(f"Lazy MF size: {sys.getsizeof(lazy_mf)} bytes")

# Process lazily
for i, value in enumerate(lazy_lc):
    if i >= 10:  # Process only first 10
        break
    print(value)

Generator expressions (parentheses instead of brackets) and map/filter without list() consume minimal memory regardless of dataset size.

Nested Comprehensions vs Multiple Maps

Flattening nested structures reveals different complexity patterns:

# Matrix transformation
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

# List comprehension: nested iteration
flattened_doubled = [x * 2 for row in matrix for x in row]

# Map equivalent: requires chain or nested map
from itertools import chain
flattened_doubled_mf = list(map(lambda x: x * 2, chain.from_iterable(matrix)))

# Alternative: nested map (less readable)
flattened_doubled_nested = list(chain.from_iterable(map(lambda row: map(lambda x: x * 2, row), matrix)))

print(flattened_doubled)  # [2, 4, 6, 8, 10, 12, 14, 16, 18]

Nested list comprehensions handle multidimensional iteration naturally. Map requires helper functions like chain.from_iterable or deeply nested calls.

Type Hints and Static Analysis

Modern Python development benefits from type checking. List comprehensions integrate better with type checkers:

from typing import List, Callable, Iterator

def process_numbers(nums: List[int]) -> List[int]:
    # Type checker understands this immediately
    return [x * 2 for x in nums if x > 0]

def process_numbers_map(nums: List[int]) -> List[int]:
    # Requires explicit type annotation on lambda
    filtered: Iterator[int] = filter(lambda x: x > 0, nums)
    mapped: Iterator[int] = map(lambda x: x * 2, filtered)
    return list(mapped)

Mypy and other type checkers infer types more accurately with list comprehensions because the expression structure is explicit.

Real-World Pattern: ETL Pipeline

Here’s a practical data transformation comparing both approaches:

import json
from datetime import datetime

# Sample log data
logs = [
    '{"timestamp": "2024-01-15T10:30:00", "level": "ERROR", "message": "Connection failed"}',
    '{"timestamp": "2024-01-15T10:31:00", "level": "INFO", "message": "Retry successful"}',
    '{"timestamp": "2024-01-15T10:32:00", "level": "ERROR", "message": "Timeout"}',
]

# List comprehension approach
def parse_errors_lc(log_lines):
    return [
        {
            "time": datetime.fromisoformat(entry["timestamp"]),
            "msg": entry["message"]
        }
        for line in log_lines
        if (entry := json.loads(line))["level"] == "ERROR"
    ]

# Map/filter approach
def parse_errors_mf(log_lines):
    parsed = map(json.loads, log_lines)
    errors = filter(lambda e: e["level"] == "ERROR", parsed)
    transformed = map(
        lambda e: {"time": datetime.fromisoformat(e["timestamp"]), "msg": e["message"]},
        errors
    )
    return list(transformed)

errors_lc = parse_errors_lc(logs)
errors_mf = parse_errors_mf(logs)

The list comprehension uses the walrus operator (:=) to parse once and filter, while map/filter creates a clear three-stage pipeline.

Decision Matrix

Choose list comprehensions when:

  • Combining filtering and transformation in one pass
  • Working with nested iterations
  • Readability is paramount for complex conditions
  • Type inference matters for static analysis

Choose map/filter when:

  • Composing with existing named functions
  • Building reusable functional pipelines
  • Working in a functional programming paradigm
  • Interfacing with libraries expecting iterables

For performance-critical code with simple transformations, benchmark your specific use case. For most applications, readability trumps the 10-20% performance difference.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.