Table of Contents
- 1) Introduction
- 2) Variables, Data Types, and Operators
- 3) Strings
- 4) Data Structures
- 5) Control Flow
- 6) Functions
- 7) Classes and Object-Oriented Programming
- 8) Decorators
- 9) Modules, Imports, and Packages
- 10) Exception Handling
- 11) Iterators and Built-in Functions
- 12) What to Read Next
- 99) Task and Self-Check
1) Introduction
Python was created by Guido van Rossum in 1991. It is the primary programming language of the Odoo framework — all server-side logic (models, business rules, controllers, tests) is written in Python.
This tutorial covers exactly the Python you need to follow Tutorial 05 — Odoo from 0 to Hero and the official Odoo 19 developer tutorials. Nothing more, nothing less. If you already know Python, skim the sections and jump to Section 99 to test yourself.
python or python3) and a terminal. You will already have these from the Git & VS Code workflow in Tutorial 02 and the Docker-based dev runtime in Tutorial 03. All examples in this tutorial can also be run directly in the Python interactive shell. Odoo itself is the subject of Tutorial 05 — you do not need it installed yet.2) Variables, Data Types, and Operators
2.1) Variables and Assignment
A variable is a name that refers to a value. Python does not require you to declare the type — it is inferred from the value you assign.
name = "Library Management" # str
page_count = 350 # int
price = 29.99 # float
is_active = True # bool
Variable names must start with a letter or underscore, contain only letters, digits, and underscores, and are case-sensitive (Name ≠ name).
2.2) Core Data Types
| Type | Example | In Odoo |
|---|---|---|
str | "Odoo 19" | Field values (fields.Char), XML IDs |
int | 42 | Record IDs, fields.Integer |
float | 3.14 | fields.Float, monetary amounts |
bool | True, False | fields.Boolean, active flag |
None | None | Empty fields, unset values |
You can check the type of any value with the built-in type() function:
>>> type(42)
<class 'int'>
>>> type("hello")
<class 'str'>
2.3) Operators
| Category | Operators | Example |
|---|---|---|
| Arithmetic | + - * / // % ** | 10 // 3 → 3 (floor division) |
| Comparison | == != < > <= >= | len(digits) == 13 |
| Logical | and or not | if book.isbn and not book._check_isbn(): |
| Membership | in not in | "base" in depends_list |
| Identity | is is not | value is None |
== to compare values; use is only to compare with None (if value is None). This is a Python convention enforced by linters.2.4) Truthiness and Falsy Values
Python treats certain values as False in a boolean context. Everything else is True.
| Falsy values | Example |
|---|---|
False | if False: |
None | if None: |
0, 0.0 | if 0: |
"" (empty string) | if "": |
[], (), {} (empty containers) | if []: |
This is heavily used in Odoo. For example, if not book.isbn: is True when the ISBN field is empty ("" or False).
3) Strings
3.1) String Basics and Common Methods
Strings are immutable sequences of characters, created with single ('...') or double ("...") quotes.
title = "Odoo Development"
print(title.upper()) # "ODOO DEVELOPMENT"
print(title.lower()) # "odoo development"
print(title.replace("Odoo", "ERP")) # "ERP Development"
print(title.startswith("Odoo")) # True
print("7".isdigit()) # True — used in ISBN validation
Useful string methods you will encounter in Odoo development:
| Method | Returns | Example |
|---|---|---|
.strip() | String with leading/trailing whitespace removed | " abc ".strip() → "abc" |
.split(sep) | List of substrings | "a,b,c".split(",") → ["a", "b", "c"] |
.join(iterable) | String from iterable | ", ".join(["a", "b"]) → "a, b" |
.replace(old, new) | String with replacements | "978-0-20".replace("-", "") → "978020" |
.isdigit() | True if all chars are digits | "9780".isdigit() → True |
3.2) f-Strings (Formatted String Literals)
f-strings (Python 3.6+) let you embed expressions inside string literals by prefixing with f and wrapping expressions in {}.
isbn = "978-0-201-53082-7"
name = "The Mythical Man-Month"
label = f"[{isbn}] {name}"
print(label)
# [978-0-201-53082-7] The Mythical Man-Month
f-strings are used extensively in Odoo for error messages, display names, and logging:
raise ValidationError(f"The ISBN {book.isbn} is not valid.")
4) Data Structures
4.1) Lists
A list is an ordered, mutable sequence. Created with square brackets [].
authors = ["Daniel Reis", "Holger Brunn"]
authors.append("Alexandre Fayolle") # add to end
print(authors[0]) # "Daniel Reis" — zero-based indexing
print(authors[-1]) # "Alexandre Fayolle" — last element
print(len(authors)) # 3
Slicing extracts a portion of the list:
digits = [9, 7, 8, 0, 2, 0, 1, 5, 3, 0, 6, 3, 6]
first_twelve = digits[:12] # [9, 7, 8, 0, 2, 0, 1, 5, 3, 0, 6, 3]
last = digits[-1] # 6
List repetition with *:
weights = [1, 3] * 6 # [1, 3, 1, 3, 1, 3, 1, 3, 1, 3, 1, 3]
4.2) Tuples
A tuple is an ordered, immutable sequence. Created with parentheses (). Tuples cannot be changed after creation.
point = (10, 20)
domain_rule = ("active", "=", True) # Odoo domain filter element
print(domain_rule[0]) # "active"
In Odoo, tuples are used for domain filters: [('active', '=', True)] — a list of tuples, where each tuple is a condition (field, operator, value).
4.3) Dictionaries
A dictionary is an unordered collection of key-value pairs. Created with curly braces {}.
book_data = {
"name": "Library Management",
"isbn": "978-0-201-53082-7",
"active": True,
}
print(book_data["name"]) # "Library Management"
book_data["pages"] = 350 # add a new key
Dictionaries are everywhere in Odoo:
create()andwrite()accept dictionaries:Book.create({"name": "Test"})- The module manifest (
__manifest__.py) is a single dictionary. - Context values:
{"group_by": "publisher_id"}
4.4) List Comprehensions
A list comprehension builds a new list by applying an expression to each element of an iterable, optionally filtering.
# Extract digits from an ISBN string
isbn = "978-0-201-53082-7"
digits = [int(x) for x in isbn if x.isdigit()]
print(digits) # [9, 7, 8, 0, 2, 0, 1, 5, 3, 0, 8, 2, 7]
General syntax: [expression for variable in iterable if condition]
# Multiply paired elements
weights = [1, 3] * 6
terms = [a * b for a, b in zip(digits[:12], weights)]
print(terms) # [9, 21, 8, 0, 2, 0, 1, 15, 3, 0, 6, 9]
zip() pairs elements from two lists: zip([1,2], [3,4]) yields (1,3), (2,4). The a, b syntax in the for clause is called tuple unpacking.5) Control Flow
5.1) if / elif / else
Python uses indentation (4 spaces) to define code blocks — no curly braces.
isbn_length = len(digits)
if isbn_length == 13:
print("ISBN-13 format")
elif isbn_length == 10:
print("ISBN-10 format")
else:
print("Invalid ISBN length")
5.2) for Loops
The for loop iterates over any iterable (list, tuple, string, dictionary, range, etc.).
# Iterate over a list
authors = ["Reis", "Brunn", "Fayolle"]
for author in authors:
print(author)
# Iterate over a dictionary
book = {"name": "Test", "isbn": "123"}
for key, value in book.items():
print(f"{key}: {value}")
In Odoo, self inside a model method is a recordset — you iterate over it with for record in self:.
def button_check_isbn(self):
for book in self: # self = recordset of books
if not book.isbn:
raise ValidationError(f"Please provide an ISBN for {book.name}")
5.3) Ternary Expressions
A one-line if/else expression (also called a conditional expression):
label = f"[{book.isbn}] {book.name}" if book.isbn else book.name
Syntax: value_if_true if condition else value_if_false
6) Functions
6.1) Defining and Calling Functions
A function groups reusable logic. Defined with the def keyword.
def check_isbn_length(isbn):
digits = [x for x in isbn if x.isdigit()]
return len(digits) == 13
result = check_isbn_length("978-0-201-53082-7")
print(result) # True
6.2) Default Arguments and Keyword Arguments
Default arguments give a parameter a fallback value. Keyword arguments pass values by parameter name, which makes calls easier to read.
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Srđan")) # "Hello, Srđan!"
print(greet("Srđan", greeting="Hi")) # "Hi, Srđan!"
6.3) *args and **kwargs
*args collects extra positional arguments into a tuple; **kwargs collects extra keyword arguments into a dictionary.
def example(*args, **kwargs):
print(args) # (1, 2, 3)
print(kwargs) # {"key": "value"}
example(1, 2, 3, key="value")
In Odoo, you will see **kwargs in controller methods:
@http.route("/library/books", auth="public", website=True)
def list(self, **kwargs): # kwargs captures URL query parameters
...
6.4) Return Values
A function returns None by default. Use return to send back a value.
def _check_isbn(self):
self.ensure_one()
digits = [int(x) for x in self.isbn if x.isdigit()]
if len(digits) == 13:
weights = [1, 3] * 6
terms = [a * b for a, b in zip(digits[:12], weights)]
remainder = sum(terms) % 10
check = 10 - remainder if remainder != 0 else 0
return digits[-1] == check # returns True or False
return False # early return if not 13 digits
7) Classes and Object-Oriented Programming
7.1) Defining a Class
A class is a blueprint for creating objects. Python classes use the class keyword.
class Book:
def __init__(self, title, isbn):
self.title = title # instance attribute
self.isbn = isbn
def describe(self):
return f"{self.title} ({self.isbn})"
b = Book("The Pragmatic Programmer", "978-0201616224")
print(b.describe()) # "The Pragmatic Programmer (978-0201616224)"
7.2) The self Parameter
Every instance method receives self as the first argument — it refers to the specific object the method was called on. You do not pass it explicitly.
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1 # self.count belongs to this specific Counter instance
c = Counter()
c.increment()
print(c.count) # 1
self inside a model method is a recordset, not a single record. You must iterate with for record in self: to access individual records. Calling self.ensure_one() asserts that the recordset contains exactly one record.7.3) Class Attributes vs Instance Attributes
Class attributes live on the class and are shared. Instance attributes live on one object created from that class.
class Book:
_description = "Library Book" # class attribute — shared by all instances
def __init__(self, title):
self.title = title # instance attribute — unique per instance
In Odoo models, fields (name = fields.Char(...)) are defined as class attributes, but the ORM makes them behave as instance attributes at runtime.
7.4) Inheritance and super()
Inheritance lets a class reuse and extend another class.
class Animal:
def speak(self):
return "..."
class Dog(Animal): # Dog inherits from Animal
def speak(self):
return "Woof!"
class Puppy(Dog):
def speak(self):
base = super().speak() # calls Dog.speak()
return f"{base} (tiny)"
p = Puppy()
print(p.speak()) # "Woof! (tiny)"
In Odoo, every model inherits from models.Model:
class Book(models.Model): # inherits ORM methods: create, write, search, ...
_name = "library.book"
7.5) Underscore Naming Conventions
| Pattern | Convention | Odoo Example |
|---|---|---|
_name | Single leading underscore — "private" by convention (not enforced) | _name = "library.book" |
__init__ | Dunder (double underscore) — special Python method | __init__.py package marker, __manifest__.py |
_check_isbn | Private method — internal helper, not called from outside the class | def _check_isbn(self): |
button_check_isbn | No underscore — public method, callable from XML views | def button_check_isbn(self): |
8) Decorators
8.1) What is a Decorator?
A decorator is a function that wraps another function to add behaviour. It is applied with the @ syntax above the function definition.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name):
return f"Hello, {name}!"
greet("World")
# Prints: Calling greet
# Returns: "Hello, World!"
The @log_call line is equivalent to greet = log_call(greet).
8.2) Built-in Decorators: @classmethod, @staticmethod, @property
Built-in decorators change how a method is bound or accessed without writing a custom wrapper.
class Book:
_count = 0
def __init__(self, title):
self.title = title
Book._count += 1
@classmethod
def get_count(cls): # receives the class, not the instance
return cls._count
@staticmethod
def is_valid_isbn(isbn): # no self or cls — a plain utility function on the class
return len(isbn) == 13
@property
def label(self): # accessed as book.label, not book.label()
return f"Book: {self.title}"
In Odoo tests, @classmethod is used for setUpClass:
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.Book = cls.env["library.book"]
8.3) Odoo Decorators Preview
Odoo defines its own decorators in the odoo.api module. You will learn them in detail in Tutorial 05. For now, know that they exist and what they do at a high level:
| Decorator | Purpose |
|---|---|
@api.depends("field1", "field2") | Marks a computed field method — recalculates when listed fields change |
@api.constrains("field") | Marks a validation method — runs on create/write, raises ValidationError on bad data |
@api.onchange("field") | Runs when a field value changes in a form (before saving) |
@api.model | Marks a class-level method that does not operate on a specific recordset |
9) Modules, Imports, and Packages
9.1) import and from ... import
A module is a single .py file. You import it to use its classes, functions, or variables.
import math
print(math.sqrt(16)) # 4.0
from math import sqrt
print(sqrt(16)) # 4.0 — imported directly
Odoo imports follow a consistent pattern:
from odoo import api, fields, models
from odoo.exceptions import ValidationError
9.2) __init__.py and Package Structure
A package is a folder containing an __init__.py file. The file marks the folder as importable and controls what gets loaded.
When Odoo loads the library module, it executes library/__init__.py, which imports the sub-packages. Each sub-package's __init__.py imports its .py files. This chain ensures all models, controllers, and tests are registered.
9.3) Relative Imports
Inside a package, use a dot (.) to import from the same package:
# library/__init__.py
from . import models # imports library/models/__init__.py
from . import controllers # imports library/controllers/__init__.py
# library/models/__init__.py
from . import library_book # imports library/models/library_book.py
The . means "the current package". This is a relative import, as opposed to an absolute import like from odoo import models.
10) Exception Handling
10.1) try / except / finally
try:
value = int("abc")
except ValueError as e:
print(f"Error: {e}") # Error: invalid literal for int() with base 10: 'abc'
finally:
print("This always runs")
You can catch specific exception types. Always catch the most specific type, not a bare except:.
10.2) raise
raise explicitly throws an exception.
from odoo.exceptions import ValidationError
def validate_isbn(isbn):
if not isbn:
raise ValidationError("ISBN cannot be empty.")
if len(isbn) != 13:
raise ValidationError(f"The ISBN {isbn} is not valid.")
In Odoo, ValidationError is the standard way to report user-facing validation failures. The framework catches it and displays the message in the UI.
10.3) Context Managers (with statement)
The with statement ensures resources are properly cleaned up (e.g. files are closed, transactions are rolled back).
# File handling
with open("data.txt", "r") as f:
content = f.read()
# f is automatically closed here, even if an exception occurred
In Odoo tests, with self.assertRaises() is a context manager that verifies an exception is raised:
with self.assertRaises(ValidationError):
self.Book.create({"name": False}) # should raise ValidationError
11) Iterators and Built-in Functions
11.1) zip(), enumerate(), range()
# zip — pair elements from two lists
names = ["Alice", "Bob"]
scores = [90, 85]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# enumerate — loop with an index
for i, name in enumerate(names):
print(f"{i}: {name}")
# range — generate a sequence of numbers
for i in range(5):
print(i) # 0, 1, 2, 3, 4
11.2) len(), int(), str(), type()
print(len([1, 2, 3])) # 3
print(int("42")) # 42
print(str(42)) # "42"
print(type(42)) # <class 'int'>
11.3) sum(), any(), all()
terms = [9, 21, 8, 0, 2, 0, 1, 15, 3, 0, 6, 9]
print(sum(terms)) # 74
print(sum(terms) % 10) # 4 — modulo for ISBN check digit
flags = [True, False, True]
print(any(flags)) # True — at least one is True
print(all(flags)) # False — not all are True
12) What to Read Next
- Tutorial 05 — Odoo from 0 to Hero — your first Odoo application, building on everything covered here.
- Official Odoo 19 Developer Tutorials — the canonical Odoo tutorial series.
- The Python Tutorial (docs.python.org) — the official Python tutorial for deeper exploration of any topic.
99) Task and Self-Check
Task: ISBN-13 Check Digit Calculator
Write a Python function isbn_check_digit(isbn_12) that takes a 12-digit string (the first 12 digits of an ISBN-13) and returns the correct check digit (an integer 0–9). Use what you learned in this tutorial.
- Extract digits — use a list comprehension to convert the string to a list of integers. Reference: 4.4.
- Validate length — if the list does not have exactly 12 elements, raise a
ValueError. Reference: 10.2. - Calculate weighted sum — create
[1, 3] * 6, usezip()to pair digits with weights, then storesum()of the products intotal. Reference: 4.4, 11.1, 11.3. - Compute check digit — use a ternary expression:
10 - (total % 10)unless the remainder is 0, in which case the check digit is 0. Reference: 5.3. - Return the check digit — Reference: 6.4.
Test your function:
assert isbn_check_digit("978020153082") == 7
assert isbn_check_digit("978144932539") == 4
print("All tests passed!")
Self-check
Key concepts — explain in your own words:
- variable, data type, truthiness
- f-string
- list, tuple, dictionary
- list comprehension, tuple unpacking
- for loop, ternary expression
- function, default argument,
**kwargs - class,
self, class attribute vs instance attribute - inheritance,
super() - decorator,
@classmethod - module, package,
__init__.py, relative import raise,try/except, context manager
You must be able to answer:
- What happens when you write
if not book.isbn:andisbnis an empty string? - What is the difference between
==andis? - Why does Odoo use
for record in self:instead of accessingself.fielddirectly? - What is the purpose of
__init__.py? - What does
@api.depends("name")tell Odoo to do? - Write a list comprehension that extracts all even numbers from
[1, 2, 3, 4, 5, 6]. - What is the difference between
raiseandreturn?