Skip to Content
KomITi Academy

Python Basics

The Python You Need Before Writing Your First Odoo Module

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.

Note: do not only read the examples. Run the short snippets in a Python shell or scratch file, then change one value and run them again.
Prerequisites: a working Python interpreter (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 (Namename).

2.2) Core Data Types

TypeExampleIn Odoo
str"Odoo 19"Field values (fields.Char), XML IDs
int42Record IDs, fields.Integer
float3.14fields.Float, monetary amounts
boolTrue, Falsefields.Boolean, active flag
NoneNoneEmpty 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

CategoryOperatorsExample
Arithmetic+ - * / // % **10 // 33 (floor division)
Comparison== != < > <= >=len(digits) == 13
Logicaland or notif book.isbn and not book._check_isbn():
Membershipin not in"base" in depends_list
Identityis is notvalue is None
Tip: use == 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 valuesExample
Falseif False:
Noneif None:
0, 0.0if 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:

MethodReturnsExample
.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() and write() 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]
Note: 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
Important: in Odoo, 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

PatternConventionOdoo Example
_nameSingle leading underscore — "private" by convention (not enforced)_name = "library.book"
__init__Dunder (double underscore) — special Python method__init__.py package marker, __manifest__.py
_check_isbnPrivate method — internal helper, not called from outside the classdef _check_isbn(self):
button_check_isbnNo underscore — public method, callable from XML viewsdef 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:

DecoratorPurpose
@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.modelMarks 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.

library/ ├── __init__.py ← from . import models, controllers ├── __manifest__.py ├── models/ │ ├── __init__.py ← from . import library_book │ └── library_book.py ├── controllers/ │ ├── __init__.py ← from . import main │ └── main.py └── tests/ ├── __init__.py ← from . import test_book └── test_book.py

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


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.

  1. Extract digits — use a list comprehension to convert the string to a list of integers. Reference: 4.4.
  2. Validate length — if the list does not have exactly 12 elements, raise a ValueError. Reference: 10.2.
  3. Calculate weighted sum — create [1, 3] * 6, use zip() to pair digits with weights, then store sum() of the products in total. Reference: 4.4, 11.1, 11.3.
  4. 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.
  5. Return the check digitReference: 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: and isbn is an empty string?
  • What is the difference between == and is?
  • Why does Odoo use for record in self: instead of accessing self.field directly?
  • 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 raise and return?