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 efficientfiltered()operates in Python; for large datasets, prefersearch()with domainssorted()sorts in Python; for large datasets, useorderparameter insearch()- Avoid calling
mapped()inside loops; call it once on the full recordset - Use
read()when you only need raw field values without ORM features