rvtkit — Pythonic wrapper for the Autodesk Revit API (2025 - 2027)
A Pythonic wrapper around the Autodesk Revit API with idiomatic ergonomics and a LINQ-style query layer. Targets Revit 2025 / 2026 / 2027 (.NET 8+ / CoreCLR) on CPython 3.12 via pythonnet.
The core needs only pythonnet and cloudpickle ; windows, web pages,
dashboards, graphs and exports are optional extras (see
Installation).
import rvtkit as rk
walls = rk.activeDoc.categories['Walls'].instances
Formerly
revit. The package was renamedrvtkit(October 2026) andimport revitno longer works : writeimport rvtkit as rk.
Autodesk and Revit are registered trademarks of Autodesk, Inc. rvtkit is not affiliated with, sponsored or endorsed by Autodesk.
Table of contents
- Compatibility
- Requirements
- Installation
- Package layout
- Documentation
- Quick start
- Live proxies (
activeDoc/activeView/app) - Document
- Categories
- Families
- Family types
- Instances / elements
- LinqList — operators
- LinqMap — operators
- IDE autocomplete (typed lambdas)
- Context managers (
selectionScope/isolated) - Jupyter / pyRevit Output rendering
- Diagnostics & banners
- Smart printing
- Full examples
- Licence
Compatibility
Revit versions
| Revit | Runtime | Bootstrap call | Status |
|---|---|---|---|
| 2025 | CPython 3.12 + pythonnet (.NET 8) | loadAssemblies("2025") |
✅ runtime-validated by author |
| 2026 | CPython 3.12 + pythonnet (.NET 8) | loadAssemblies("2026") |
✅ primary target — 544 tests in CI |
| 2027 | CPython 3.12 + pythonnet (.NET 10) | loadAssemblies("2027") |
✅ runtime-validated by author |
Revit 2025+ runs on .NET 8 (2027 on .NET 10) / CoreCLR and requires
CPython 3.12 + pythonnet 3.x. See rvtkit.bootstrap and the embedded
runtime configs in rvtkit/_runtimeconfigs/.
Note: Revit 2020 - 2024 (.NET Framework 4.8 / IronPython) is no longer supported as of
0.1.3. Pin to0.1.2if you need legacy Revit support.
Launchers
| Launcher | Injected variable | Status |
|---|---|---|
| pyRevit (CPython mode) | __revit__ |
✅ |
| Custom .NET addin / shell host (CPython 3.12) | __uiapp__ / __revit__ |
✅ |
| RevitPythonShell | __revit__ |
❌ IronPython only — not supported since 0.1.3 |
Requirements
- Revit 2025, 2026, or 2027 (.NET 8+ / CoreCLR host)
- CPython 3.12 or 3.13 (
requires-python = ">=3.12,<3.14"enforced by pip) pythonnet >= 3.0— installed automatically bypip installon Windows (declared aspythonnet>=3.0; sys_platform == 'win32', so Linux/macOS CI runs skip it)cloudpickle >= 3.0— installed automatically (the remote mode,rvtkit.connect(), sends functions to Revit with it)- Everything else is an optional extra — see Installation
External dependencies (host-provided, not on PyPI)
Pulled in at runtime via clr / pythonnet from whichever Revit version
is currently running.
| Dependency | Source | Used for |
|---|---|---|
Autodesk.Revit.DB |
Revit install | Core API — required everywhere |
Autodesk.Revit.UI |
Revit install | Dialogs, selection helpers (lazy import) |
clr |
pythonnet | .NET interop (assembly loading) |
System.Reflection, System.Runtime.Loader |
.NET BCL | Bootstrap (assembly resolver) |
System.Collections.Generic, System.IO, System.Xml, System.Drawing.Imaging, System.Diagnostics |
.NET BCL | Various wrappers, lazy-loaded |
Blender (rvtkit.blender) and ParaView (rvtkit.paraview) bring their own
Python : the bridges run inside them.
Installation
pip install rvtkit
The distribution and the import are both named rvtkit (pip install rvtkit, import rvtkit).
The core is light : what needs a heavier library is an extra, installed with
pip install "rvtkit[qt,web]" (several at once) or pip install "rvtkit[all]".
| Extra | Installs | For |
|---|---|---|
qt |
PySide6, tomli-w | windows with widgets (rvtkit.app), themes and icons (rvtkit.ui), the Revit widgets of Qt Designer, the Qt 3D viewer, revit-ui, revit-plugin |
web |
pywebview | a web page as the interface (rvtkit.web, revit-web), a dashboard's desktop window |
dash |
dash, dash-bootstrap-components, plotly, pandas | live dashboards (rvtkit.dashkit, revit-dash) |
pandas |
pandas | .to_df() and the other DataFrame exports |
excel |
pandas, openpyxl | Schedule.to_excel() |
bi |
pandas, pyarrow, duckdb | rvtkit.dashboard : Parquet / DuckDB for Power BI, Tableau… |
mep |
networkx, pyvis, pydot | MEP networks as graphs |
marimo |
marimo | rvtkit.editor, the bulk parameter editor |
animation |
imageio[ffmpeg], pillow | rvtkit.Recorder (MP4, GIF, WebP) |
gltf |
pygltflib | reading external .gltf files in the 3D viewer |
console |
qtconsole, ipykernel | the embedded Python console widget |
ai |
rank_bm25, mcp | rvtkit.ai : corpus search and the revit-mcp server |
all |
all of the above |
The 3D viewer runs xeokit (AGPL-3.0), which rvtkit does not ship : the first
viewer that needs it downloads it once from jsDelivr into
%LOCALAPPDATA%\rvtkit\xeokit and works offline afterwards.
Configuring the host inside Revit
In your startup script (pyRevit CPython mode, or a custom CPython addin/shell host):
import rvtkit
rvtkit.init() # walks the call stack to find __revit__ or __uiapp__
init() is also called automatically when you import rvtkit, so in most
hosts (RPS console, pyRevit, custom addin where __revit__ lives in globals
or builtins) you don't need to call it explicitly. Re-call it after a
rvtkit.reload() or when entering a new scope where the host changed.
If no host is injected anywhere on the call stack, fall back to the explicit form:
import rvtkit
rvtkit.set_host(__revit__) # or __uiapp__ depending on the launcher
Package layout
revit/
├── __init__.py # getApp / getDoc / getDocs / set_host / init / Print / run_test
├── _host.py # UIApplication resolution
├── _accessors.py # RevitContextError + import boundaries
├── _app.py # Application (renamed from app.py in 0.1.5)
├── _print.py # rvtkit.Print() — package banner
├── _runtest.py # rvtkit.run_test() — in-Revit functional check
├── _wrappers.py # ElementWrapper (base) + parameter access
├── doc.py # Document ← main hub (carved into _doc_* mixins)
├── category.py # CategoryWrapper
├── family.py # FamilyWrapper / SystemFamilyWrapper / SystemFamilyTypeWrapper
├── elements.py # FamilyTypeWrapper / InstanceWrapper / ViewWrapper
└── linq/
├── __init__.py # exposes LinqList, LinqMap (aliased as List, Map)
├── linqlist.py # LinqList[T] — chainable collection
├── linqmap.py # LinqMap[K, V] — chainable dict (returned by GroupBy)
└── _exporters.py # JSON / XML / TOML / YAML writers
Documentation
Most doc-level collections (materials, views, sheets, families,
familyTypes, categories, levels, grids, phases, worksets,
viewTemplates, scopeBoxes, filters, titleBlocks, revisions, …)
are subscriptable by name — prefer doc.categories['Walls'] over
doc.getCategory('Walls'). Both forms exist; the bracket form is more
idiomatic and keeps queries terse.
Quick start
import rvtkit
app = rvtkit.getApp()
doc = rvtkit.getDoc()
Live proxies
Three module-level attributes track the current host state, so user code
doesn't have to re-call getApp() / getDoc() on every line:
rvtkit.app # cached Application (single instance per process)
rvtkit.activeDoc # live proxy that always forwards to the active Document
rvtkit.activeView # live proxy that always forwards to the active view
rvtkit.app is resolved on first access (needs the .NET assemblies loaded)
and memoised. rvtkit.activeDoc / rvtkit.activeView are live forwarders
— they re-resolve the active doc/view on every attribute access, so they
remain correct after the user switches documents in Revit:
rvtkit.activeDoc.title # current document title
rvtkit.activeDoc.materials['Aluminium'] # current document materials
rvtkit.activeView.name # current view name
rvtkit.app.version_name # 'Autodesk Revit 2026'
Migration (0.1.4): the old
rvtkit.doc/rvtkit.viewproxies were renamed torvtkit.activeDoc/rvtkit.activeView— the old names were shadowed by thervtkit.doc/rvtkit.viewsubmodules at first lazy import. Update any pre-0.1.4 user code accordingly.
Document
doc = rvtkit.getDoc()
doc.title # file name
doc.path # full path
doc.is_workshared # True/False
doc.view # active view (alias for activeView)
doc.categories['Walls'] # CategoryWrapper
doc.families['Basic Wall'] # SystemFamilyWrapper
doc.families['M_Fixed'] # FamilyWrapper (loadable family)
doc.familyTypes['Generic - 200mm'] # FamilyType wrapper (loadable + system)
doc.materials['Aluminium'] # MaterialWrapper
doc.levels['Level 1'] # Level wrapper
doc.getElementById(123456) # wrapper from an integer id
doc.<collection> returns a LinqList you can iterate, filter, or look up
by name. See doc.py for the full list.
Categories
cat = doc.categories['Walls'] # or 'Doors', 'Floors', etc.
cat.name # 'Walls'
cat.getFamilies() # LinqList[FamilyWrapper | SystemFamilyWrapper]
cat.getFamilyTypes() # LinqList[FamilyTypeWrapper | SystemFamilyTypeWrapper]
cat.getInstances() # LinqList[ElementWrapper | InstanceWrapper]
Loadable / system detection is automatic and dynamic — no hard-coded list. Works for every Revit category (architecture, MEP, structure, etc.).
Families
# Loadable
fam = doc.families['M_Fixed']
fam.name, fam.getFamilyTypes(), fam.getInstances()
# System (walls, floors, roofs…)
fam = doc.families['Basic Wall']
fam.getFamilyTypes()[0] # first type via index
Family types
ft = doc.familyTypes['Generic - 200mm']
ft.name, ft.family_name, ft.id
# Parameter access — by name, with unit conversion helpers
ft['Width'] # raw value (Revit internal feet)
ft.mm('Width') # in millimetres
ft.m('Width') # in metres
ft.params # LinqList of ParamWrapper, subscriptable
ft.params['Width'].value # explicit form
ft.getInstances() # LinqList[ElementWrapper] of this type
Instances / elements
inst = doc.categories['Doors'].getInstances().First()
inst.id, inst.name, inst.category, inst.family_name
inst.family_type # FamilyTypeWrapper (navigable)
inst.family # FamilyWrapper
# Parameters — subscriptable, both read and write
inst['Mark'] # read
inst.params['Mark'] = 'W1' # write (transaction implicit when none active)
inst.mm('Height')
inst.location # (x, y, z) or LocationCurve
LinqList — operators
LinqList is returned by every collection method. Intermediate operations
are lazy (evaluated on demand).
| Category | Operators |
|---|---|
| Filter / project | Where Select SelectMany Distinct Partition |
| Order | OrderBy OrderByDescending Reverse |
| Slice | Take Skip TakeWhile SkipWhile |
| Set algebra | Union Intersect Except Concat Zip Aggregate (also | & - +) |
| Itertools | Pairwise Window(n) Chunk(n) |
| Group | GroupBy → LinqMap ; CountBy → LinqMap[K, int] |
| Joining | Join (INNER) GroupJoin DefaultIfEmpty |
| Terminals | First FirstOrDefault Last Single Any All Count Sum Min Max Average MinBy MaxBy Contains |
| Convert | ToList ToDict |
| Side effects | ForEach Tap Print |
| Revit shortcut | Instances (= SelectMany(t => t.getInstances())) |
| Pandas (QA / reporting) | to_df query pivot_table value_counts describe |
# Typical chain
doc.categories['Walls'].getFamilyTypes() \
.Where(lambda t: (t.mm('Width') or 0) > 200) \
.OrderByDescending(lambda t: t.mm('Width')) \
.Take(5) \
.Select(lambda t: t.name)
QA / reporting via pandas
For BIM QA audits and reporting deliverables, LinqList exposes a
pandas-borrowed shortcut layer. query() returns a
LinqList[Wrapper] (not a DataFrame), so chaining stays natural;
pivot_table / value_counts / describe produce ready-to-export
analytical outputs.
walls = doc.categories['Walls'].instances
# SQL-like filter, round-trip to wrappers
walls.query("Area > 50 and not `Fire Rating`") \
.to_df(['Mark', 'Level', 'Area']) \
.to_excel('audit_CF.xlsx')
# Pivot — area per level × wall type
walls.pivot_table('Area', index='Level',
columns='Type', aggfunc='sum')
Requires pip install rvtkit[pandas].
LinqMap — operators
LinqMap is returned by GroupBy(). Each value is a LinqList.
groups = doc.categories['Walls'].getFamilyTypes() \
.GroupBy(lambda t: t.family_name)
groups['Basic Wall'] # LinqList of Basic Wall types
groups.Keys() # LinqList of keys
groups.Where(lambda k, v: v.Count() > 2) # filter the groups
groups.Select(lambda k, v: (k, v.Count())) # project → LinqList of tuples
groups.Print(lambda k, v: '{}: {}'.format(k, v.Count()))
groups.Flatten() # flat LinqList of every element
LinqMap is Generic[K, V], so the k and v lambda parameters carry
the right types end-to-end — see the next section.
IDE autocomplete (typed lambdas)
LinqList and LinqMap are generic (LinqList[T] / LinqMap[K, V]),
and the doc-level collections expose typed return signatures, so Pylance,
Pyright, Jedi, and PyCharm propose wrapper members on lambda parameters:
rvtkit.activeDoc.materials.Where(lambda m: m.<TAB>) # → MaterialWrapper members
rvtkit.activeDoc.getElements().Select(lambda e: e.<TAB>) # → ElementWrapper members
# Group + filter — k is str, v is LinqList[ElementWrapper]
rvtkit.activeDoc.getElements() \
.GroupBy(lambda e: e.category.name) \
.Where(lambda k, v: k.<TAB>) # → str members
Subscript chains preserve the wrapper type too:
rvtkit.activeDoc.materials['Aluminium'].thermal.<TAB> # → ThermalWrapper members
rvtkit.activeDoc.familyTypes['Generic - 200mm'].mm('<TAB>') # parameter helpers
Context managers
Two with-blocks save/restore Revit UI state automatically — even when the
body raises:
# Save the current selection, set a new one, restore on exit
with rvtkit.activeDoc.selectionScope(walls):
... # selection restored, even on exception
# Temporarily isolate elements in a view, restore full visibility on exit
with rvtkit.activeView.isolated(walls):
rvtkit.activeView.Print()
...
isolated() uses IsolateElementsTemporary (session-only, not persisted on
save) but is still wrapped in a SubTransaction so it composes with any
outer TransactionWrapper.
Jupyter / pyRevit Output rendering
LinqList and LinqMap both implement _repr_html_, so they render as
proper tables in Jupyter notebooks and in the pyRevit Output window.
No explicit call needed — just leave the value as the last expression of a
cell:
rvtkit.activeDoc.materials \
.Where(lambda m: m.thermal is not None) \
.Select(lambda m: (m.name, m.thermal.thermalConductivity))
# → HTML table in Jupyter / pyRevit Output, plain repr in a terminal
Diagnostics & banners
Three Print() surfaces describe what the package sees, at three
different scopes:
rvtkit.Print() # the package itself — version, install path,
# Python runtime, host status, loaded submodules.
# Works *before* a host is bound (handy for
# diagnosing import problems in plain CPython).
rvtkit.app.Print() # the running Revit host — VersionName, build,
# user, # documents open, active doc, memory,
# PID, uptime.
rvtkit.activeDoc.Print() # the active document — title, path, workshared
# status, # elements, # families, etc.
Other diagnostics:
rvtkit.submodules # sorted LinqList[str] of loaded rvtkit.* modules
rvtkit.reload() # force-reload every rvtkit.* submodule
# (returns LinqList[str] of reloaded names)
rvtkit.run_test() # in-Revit functional check — PASS/FAIL table
# over import / host / doc / proxies / linq
# / params / transaction / Print
Errors raised when the package is exercised outside a Revit process
(no Autodesk.Revit.DB assemblies, no host bound) come out as a single
RevitContextError(RuntimeError) with a two-frame traceback — no
seven-deep ModuleNotFoundError chain to wade through:
from rvtkit import RevitContextError
try:
rvtkit.getDoc()
except RevitContextError as e:
print('not running inside Revit:', e)
Smart printing
Every wrapper and collection defines its own multi-line __str__ — just use print():
print(doc.families['Basic Wall'])
print(doc.categories['Walls'].getFamilyTypes())
doc.categories['Windows'].getFamilies().Print(lambda f: f.name)
Full examples
import rvtkit
doc = rvtkit.getDoc()
# Walls thicker than 200 mm, sorted desc
doc.categories['Walls'].getFamilyTypes() \
.Where(lambda t: (t.mm('Width') or 0) > 200) \
.OrderByDescending(lambda t: t.mm('Width')) \
.Print(lambda t: '{:40s} {:>8.1f} mm'.format(t.name, t.mm('Width')))
# Instance count per window family
doc.categories['Windows'].getFamilies() \
.GroupBy(lambda f: f.name) \
.Select(lambda k, v: (k, v.Flatten().Count())) \
.ForEach(lambda pair: print('{}: {} instance(s)'.format(*pair)))
# Navigation from an instance
inst = doc.categories['Doors'].getInstances().First()
print(inst.family_name, inst.family_type.name, inst.family_type.mm('Height'))
Licence
MIT (see LICENSE). The wheel also ships a few third-party files (AvalonDock,
ModernWpf, FilterDataGrid, PyQtDarkTheme, Material Symbols, the WebView2 SDK),
each under its own licence : see THIRD_PARTY_NOTICES.md, installed with the
package's metadata. xeokit (AGPL-3.0), which the 3D viewer runs, is not part of
rvtkit : it is downloaded on first use (see Installation).
Metadata
Release files for rvtkit 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rvtkit-0.2.1.tar.gz | 8.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rvtkit-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 16.2 MB
Release files / rvtkit-0.2.1.tar.gz
| Download URL | rvtkit-0.2.1.tar.gz |
|---|---|
| Size | 8.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
76438f185405bc15ebe38e50137c64204bf5c64135fc515867a5512aa3defc00
|
|
BLAKE2b-256 checksum How to use checksums |
c3e361bc80c13e3045889614ac37a4a4dc1dc97659a6a1864e1be43197f402b0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / rvtkit-0.2.1-py3-none-any.whl
| Download URL | rvtkit-0.2.1-py3-none-any.whl |
|---|---|
| Size | 8.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4e68f012649392f336b60d78957ed3077f3a9a5976888a97d33527b7e8077f4f
|
|
BLAKE2b-256 checksum How to use checksums |
416a8e7f85d451f2d743b988501d31eaf5c1a3fbf1a2f0bbecdb389b0a01b650
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|