Skip to content

Odoo Recordset Methods: mapped, filtered, sorted, and Beyond

DeployMonkey Team · March 23, 2026 12 min read

Recordsets in Odoo

A recordset is an ordered collection of records from the same model. Every ORM operation in Odoo works with recordsets, even single records (a recordset of length 1). Understanding recordset methods is essential for writing clean, efficient Odoo code.

Creating Recordsets

# Search returns a recordset
partners = self.env['res.partner'].search(
    [('is_company', '=', True)])

# Browse by IDs
partners = self.env['res.partner'].browse([1, 2, 3])

# Empty recordset
empty = self.env['res.partner']
len(empty)  # 0
bool(empty)  # False

# Single record (recordset of length 1)
partner = self.env['res.partner'].browse(1)
len(partner)  # 1

mapped() — Extract and Transform

The mapped() method extracts field values or applies a function to each record:

# Extract field values as a list
names = partners.mapped('name')  # ['Company A', 'Company B']

# Follow relational fields (dot notation)
emails = orders.mapped('partner_id.email')
# Returns flat list of all partner emails

# Get related recordsets (Many2one)
all_partners = orders.mapped('partner_id')
# Returns a recordset (deduplicated!)

# Follow One2many/Many2many
all_lines = orders.mapped('line_ids')
# Returns combined recordset of all lines

# Chain dotted paths
product_names = orders.mapped('line_ids.product_id.name')
# Returns flat list of all product names across all lines

# Apply a function
totals = orders.mapped(lambda o: o.amount_total * 1.1)
# Returns a list of computed values

mapped() for Relational vs Scalar Fields

Important distinction: when mapped() follows a relational field, it returns a recordset (deduplicated). When it accesses a scalar field (Char, Integer, etc.), it returns a plain Python list:

# Relational: returns recordset
partner_rs = orders.mapped('partner_id')  # res.partner recordset
type(partner_rs)  # odoo.api.res.partner

# Scalar: returns list
names = orders.mapped('partner_id.name')  # ['Alice', 'Bob']
type(names)  # list

filtered() — Conditional Selection

The filtered() method returns a subset of records matching a condition:

# Filter by lambda
done_orders = orders.filtered(lambda o: o.state == 'done')

# Multiple conditions
urgent_open = tickets.filtered(
    lambda t: t.priority == '3' and t.state != 'done')

# Filter by truthy field value (shortcut)
with_email = partners.filtered('email')
# Same as: partners.filtered(lambda p: p.email)

# Negation pattern
without_email = partners.filtered(
    lambda p: not p.email)

filtered() vs search()

# filtered() works on already-loaded recordsets (Python-side)
done = orders.filtered(lambda o: o.state == 'done')

# search() queries the database (SQL-side)
done = self.env['sale.order'].search(
    [('state', '=', 'done')])

# Use search() when you do not have the recordset yet
# Use filtered() when you already have records loaded

sorted() — Ordering Records

The sorted() method returns records in a specific order:

# Sort by field name (ascending)
by_name = partners.sorted('name')

# Sort descending
by_date = orders.sorted('create_date', reverse=True)

# Sort by lambda (custom key)
by_priority = tickets.sorted(
    key=lambda t: int(t.priority), reverse=True)

# Sort by multiple fields
by_state_name = orders.sorted(
    key=lambda o: (o.state, o.name))

Set Operations

Recordsets support set operations using operators:

# Union (combine recordsets, deduplicated)
all_records = recordset_a | recordset_b

# Intersection (records in both)
common = recordset_a & recordset_b

# Difference (in A but not in B)
only_a = recordset_a - recordset_b

# Membership check
if record in recordset:
    pass

# Equality (same records, regardless of order)
if recordset_a == recordset_b:
    pass

Iteration and Indexing

# Iterate over records
for partner in partners:
    print(partner.name)  # each iteration yields a single-record recordset

# Index access
first = partners[0]    # first record (recordset of 1)
last = partners[-1]    # last record
slice = partners[2:5]  # records at index 2, 3, 4

# Ensure single record (raises if len != 1)
partner = partners.ensure_one()

# Check if empty
if not partners:
    print('No partners found')

exists()

Filter out records that have been deleted from the database:

# After deletion, a recordset may contain stale IDs
record.unlink()
record.exists()  # empty recordset

# Useful pattern for safe access
if record.exists():
    print(record.name)

# Filter multiple records
valid = records.exists()  # only records still in DB

ids Property

# Get list of record IDs
id_list = partners.ids  # [1, 5, 12, 34]

# Useful for domains
orders = self.env['sale.order'].search([
    ('partner_id', 'in', partners.ids)
])

read() for Raw Data

# Get field values as list of dicts (no recordset overhead)
data = partners.read(['name', 'email', 'phone'])
# [{'id': 1, 'name': 'Alice', 'email': '...'}, ...]

# Faster than iterating recordsets when you need plain data

Combining Methods

# Chain mapped, filtered, sorted
urgent_assignees = tickets \
    .filtered(lambda t: t.priority == '3') \
    .mapped('user_id') \
    .sorted('name')

# Get emails of customers with unpaid invoices
customer_emails = invoices \
    .filtered(lambda i: i.payment_state == 'not_paid') \
    .mapped('partner_id.email')

# Sum a field
total_amount = sum(orders.mapped('amount_total'))

# Group pattern (manual)
from itertools import groupby
for state, group in groupby(
        orders.sorted('state'),
        key=lambda o: o.state):
    group_records = self.env['sale.order'].concat(*group)
    print(f'{state}: {len(group_records)} orders')

concat() and Recordset Construction

# Combine records into one recordset
from functools import reduce
all_records = reduce(lambda a, b: a | b, recordset_list)

# Or use concat
all_records = self.env['sale.order'].concat(*record_list)

Performance Tips

  • mapped() on relational fields triggers SQL prefetching, which is efficient
  • filtered() operates in Python; for large datasets, prefer search() with domains
  • sorted() sorts in Python; for large datasets, use order parameter in search()
  • Avoid calling mapped() inside loops; call it once on the full recordset
  • Use read() when you only need raw field values without ORM features