A Django implementation for PostgreSQL's ltree extension, providing efficient storage and querying of hierarchical tree-like data.
See PostgreSQL's ltree documentation to learn more about it.
The main benefits of ltree:
- Efficient path queries (ancestors, descendants, pattern matching)
- Index-friendly hierarchical storage
- Powerful label path searching
- Native PostgreSQL performance for tree operations
- Django model fields for ltree data types
- Query utilities for common tree operations
- Migration support for ltree extension installation
- Compatibility with Django's ORM and query syntax
- Django 5.2+
- Python 3.11+
- PostgreSQL 16+ (with ltree extension enabled)
-
Install the package:
pip install django-ltree
-
Add to your
INSTALLED_APPS:INSTALLED_APPS = [ ... "django_ltree", ... ]
-
Run migrations to install the ltree extension:
python manage.py migrate django_ltree
django-ltree provides a base model class called TreeModel.
TreeModel does these things out of the box:
- adds a field called
pathto your model (by default, path is created by items Id plus parent's path) - adds
t_objectswhich is theTreeManageryou can use to work with tree data - adds two indexes for
path(oneBTreeIndex, oneGistIndex) - orders items base on
path
if you are overriding the Meta class of your model, you may want to inherit from TreeModel.Meta.
class Meta(TreeModel.Meta):to keep the indexes and ordering.
-
inherit from TreeModel:
from django_ltree.models import TreeModel class Category(TreeModel): name = models.CharField(max_length=50)
-
Create tree nodes:
# make an item without a parent (root) root = Category.t_objects.create(name="Root") # make a child item child = Category.t_objects.create_child(name="Child", parent=root) # you can also use `add_child` directly on root child2 = root.add_child(name="another child")
note that path is handled by django-ltree, you don't need to pass any value for it
-
Query ancestors and descendants:
# Get all ancestors child.ancestors() # Get all descendants child.descendants()
paths are made using the objects id and (if exists) it's parent's path.
if you need to use a different field for path generation, configure it like this:
class Role(TreeModel):
name = CharField()
t_objects = TreeManager(path_field="name")now paths are created using the name field
su = Role.t_objects.create(name="SuperUser")
print(su.path) # SuperUser
admin = su.add_child(name="Admin")
print(admin.path) # SuperUser.Adminwhen using an alternative field for path generation, it is recommended to use a field that ensures uniqueness to avoid confilicts.
if you are using a field that is not auto-generated (like name in the example
above), it is recommended to overwrite TreeManager.create and
TreeManager.create_child like this:
class MyTreeManager(TreeManager):
def create(self, **kwargs):
"""create an item with no parents (root)"""
kwargs["path"] = PathValue([kwargs[self.path_field]])
obj = self._create(**kwargs)
return obj
def create_child(self, parent: "TreeModel | PathValue | None" = None, **kwargs):
if not parent:
return self.create(**kwargs)
prefix = parent.path if isinstance(parent, models.Model) else parent
kwargs["path"] = PathValue([*prefix, kwargs[self.path_field]])
obj = self._create(**kwargs)
return objfor slightly better performance and less overhead.
this does not work for auto-generated fields like id.
integer ids are zero-padded automatically so siblings sort in numeric order (see the sibling ordering section below). UUIDv7 primary keys are an alternative that needs no padding at all:
- labels are fixed-width (36 characters), so the lexicographic order
ltreeuses is consistent - UUIDv7 values start with a timestamp, so siblings sort in creation order,
and the default
ordering = ("path",)gives you a correct depth-first traversal for free
try:
from uuid import uuid7 # Python 3.14+
except ImportError:
from uuid6 import uuid7 # pip install uuid6
class Category(TreeModel):
id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
name = models.CharField(max_length=50)root = Category.t_objects.create(name="Root")
print(root.path) # 0192b1f0-3b7a-7cc3-98c4-dc0c0c07398fltree compares labels as text, so variable-width integer labels would sort
incorrectly (10 before 2). to avoid this, integer labels are zero-padded
automatically.
root = Category.t_objects.create(name="Root")
print(root.path) # 0000000000000000001manager and queryset methods (children, descendants_of, ancestors_of,
create_child, change_parent) accept unpadded values and normalize them,
so Category.t_objects.descendants_of("1.10") just works. raw ORM filters
like filter(path__match=...) are not normalized, pad those labels yourself.
existing rows have unpadded labels, pad them once in a data migration:
from django_ltree.utils import pad_path_labels
def forwards(apps, schema_editor):
pad_path_labels(
apps.get_model("myapp", "Category"),
label_width=19, # 10 if the model uses AutoField ids
using=schema_editor.connection.alias,
)if you prefer to keep unpadded paths, opt out with
t_objects = TreeManager(label_width=None).
TreeManager(label_width=<int>)forces a specific width. creating an item whose label does not fit in the width raisesValueErrorTreeManager(label_width=None)disables padding (the pre-0.8 behavior)- override
TreeManager.format_label(value)for a custom label encoding (for example base62): it receives thepath_fieldvalue of the item and returns the label string
TreeModel has the following methods:
-
label(self): returns the last part ofpath -
ancestors(self): return all the ancestors of the current item, including the item itself (uset_objects.ancestors_of(item)if you don't want the item included) -
descendants(self): return all the descendants of the current item, including the item itself (uset_objects.descendants_of(item)if you don't want the item included) -
parent(self): return the immediate parent of the current item, orNoneif the item is a root -
get_root(self): return the root parent of this item -
children(self): return all the immediate children of the current item -
siblings(self): return all the siblings of the current item (items that share the same parent with this item), not including the item itself -
add_child(self, **kwargs): create a child for this item kwargs are the arguments used to make the child (the model fields) -
get_ancestors_paths(self): return the paths of all the ancestors of the current item (not including the item's own path) as a list ofPathValue -
change_parent(self, new_parent): change the parent of the current item (this moves the item and all it's descendants to be under another item) new_parent is either a object of the same model, or thepathvalue of an object returns the number of rows updated -
make_root(self): move the current item to be a root item (moves the item and all it's descendants) returns the number of rows updated -
delete(self, cascade=False, **kwargs): deletes the current item if cascade is True, all the descendants are also deleted, otherwise the children will move under the deleted item's parent (or become root items if the deleted item was a root) -
delete_cascade(self, **kwargs): delete the current item and all it's descendants
TreeManager has the following methods
-
create_child(self, parent=None, **kwargs): creates an item ifparentis provided, it will become the parent item of the created item, otherwise creation will happen as rootparentcan be a model instance or aPathValuekwargsare the model fields used to create the item (anypathpassed in is ignored, it is always generated) -
create(self, **kwargs): create a root itemkwargsare the model fields used to create the item (anypathpassed in is ignored, it is always generated) -
roots(self): return all the root items from database -
children(self, node): return all the immediate children ofnodenodecan be a model instance, aPathValue, a string like"1.2.3", or a list of labels -
descendants_of(self, node, include_self=False, max_depth=None): return the descendants ofnodeby default the node itself is not included, passinclude_self=Trueto include itmax_depthlimits how many levels below the node to include this compiles to a single indexedlquerymatch, e.g.path ~ '1.2.*{1,3}' -
ancestors_of(self, node, include_self=False): return the ancestors ofnodeby default the node itself is not included, passinclude_self=Trueto include it
# the whole subtree under a category, excluding the category itself
Category.t_objects.descendants_of(category)
# only two levels deep, e.g. for building a menu
Category.t_objects.descendants_of(category, max_depth=2)
# breadcrumbs: all ancestors from the root down, including the item
Category.t_objects.ancestors_of(category, include_self=True)roots, children, descendants_of, and ancestors_of are also available on
querysets, so they can be chained with regular filters:
Category.t_objects.filter(is_active=True).descendants_of(category, max_depth=2)for a list of all available operations and functions for ltree check https://www.postgresql.org/docs/current/ltree.html#LTREE-OPS-FUNCS
-
exact(same as=in postgresql)TreeModel.t_objects.filter(path__exact=path) -
ancestors(same as@>in postgresql)TreeModel.t_objects.filter(path__ancestors=path) -
descendants(same as<@in postgresql)TreeModel.t_objects.filter(path__descendants=path) -
match(same as~in postgresql)TreeModel.t_objects.filter(path__match=f"{self.path}.*{{1}}") -
contains(same as?in postgresql) takes a list (or tuple) of lquery patterns and matches items whose path matches any of them; passing a single string raises aTypeErrorTreeModel.t_objects.filter(path__contains=["1.*", "2.*"]) -
depth(callsNLEVELfunction from postgresql)TreeModel.t_objects.filter(path__depth=len(path) + 1)it is a transform, so it can be combined with other lookups, e.g.path__depth__lt=3
-
django_ltree.functions.NLevelsame as NLEVEL function from postgresql -
django_ltree.functions.Subpathsame asSUBPATHfunctions from postgresql
for concatenation (||) you can use django.db.models.functions.Concat
For complete documentation, see [TODO: Add Documentation Link].
- Source Code: https://github.com/mariocesar/django-ltree
- Bug Reports: https://github.com/mariocesar/django-ltree/issues
- PyPI Package: https://pypi.org/project/django-ltree/
- PostgreSQL ltree Docs: https://www.postgresql.org/docs/current/ltree.html
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.