Configuring TIA Portal Openness Python API in Jupyter Notebooks

David Krause13 min read
SiemensTIA PortalTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

Siemens TIA Portal Openness is a .NET automation API that exposes the TIA Portal project model to external code. Combined with Python through the pythonnet bridge, Openness becomes a high-leverage scripting surface for batch project generation, bulk I/O wiring, code template instantiation, and reproducible engineering workflows. Jupyter Notebooks push that further by replacing monolithic .py scripts with cells that can be re-executed individually, eliminating the TIA Portal V19 startup hit every time you iterate on tag-table or block-generation logic.

The integration is not seamless out of the box. The combination of the IPython kernel host, .NET assembly probing rules, and the Openness Siemens.Engineering.Contract.dll dependency produces a specific failure that does not occur in vanilla python.exe: a FileNotFoundException for Siemens.Engineering.Contract the moment you try to import Siemens.Engineering. This article documents the cause, the working workaround, the V20 changes, and the engineering patterns that make Openness + Jupyter a stable day-to-day tool on TIA Portal V19 and V20.

Prerequisites and Environment Setup

Openness is an optional component of the TIA Portal installer. Confirm the following before writing any Python code:

  1. TIA Portal V19 (or V20) installed with the Openness package selected. In the Siemens installation framework, expand SIMATIC TIA Portal and tick Openness under common components. Re-run the installer in modify mode if it is not already enabled.
  2. The Openness assemblies live under C:\Program Files\Siemens\Automation\Portal V19\Bin\PublicAPI\ for V19 and Portal V20\Bin\PublicAPI\ for V20. The PublicAPI folder contains the managed DLLs that Python will resolve against.
  3. Python 3.10 to 3.12, 64-bit only. TIA Portal assemblies are 64-bit only; a 32-bit Python build will refuse to load them.
  4. pythonnet package installed via pip install pythonnet. Use pythonnet>=3.0.3 for stable Python 3.11 / 3.12 support.
  5. Jupyter installed: pip install jupyterlab ipykernel.
  6. The .NET runtime that matches the TIA Portal version on the workstation. V19 ships with .NET Framework 4.8; V20 also depends on the workstation having a desktop .NET runtime available for pythonnet to attach to.

Reference the Siemens Industry Online Support portal for the TIA Portal V19 and V20 release entries, including the Openness package contents and runtime matrix. The TIA Portal help system installed with Openness is the authoritative API reference and is opened from Start → Siemens Automation → TIA Portal Openness Documentation after install.

Component V19 V20
Openness installer path ...\Portal V19\Bin\PublicAPI\ ...\Portal V20\Bin\PublicAPI\
Primary assembly Siemens.Engineering.dll Siemens.Engineering.dll
Contract assembly Siemens.Engineering.Contract.dll Siemens.Engineering.Contract.dll
Python support Community-validated via pythonnet Improved Python support in published notes
.NET runtime required .NET Framework 4.8 .NET Framework 4.8 + desktop runtime for pythonnet
License required Yes (Openness checkbox + licence ticket) Yes (Openness checkbox + licence ticket)

Standard Python Script vs Jupyter Notebook Workflow

The classic Openness + Python pattern is a single .py file that performs the entire workflow in one shot. A minimum viable script looks like this:

import clr
clr.AddReference('Siemens.Engineering.dll')
clr.AddReference('Siemens.Engineering.Contract.dll')
from Siemens.Engineering import TiaPortal

with TiaPortal(attributed_assembly_path) as tia:
    project = tia.Projects.CreateWithTemplate(
        r'C:\Templates\Default.ap19',
        r'C:\Work\new_project.ap19')
    print(project.Name)

The pattern runs once, instantiates the Openness process, creates a project, prints its name, and disposes the TiaPortal instance. The TIA Portal UI rises to the foreground for the lifetime of the TiaPortal object, and on V19 the shell must finish loading before the first Openness call returns. That load is blocking and slow.

Jupyter reorganises that workflow into cells:

  • Cell 1: imports and creates the TiaPortal instance. Re-execution is safe because clr.AddReference is idempotent at the Python level.
  • Cell 2: opens or creates a project. Re-execution disposes the previous Project first.
  • Cell 3: adds devices, blocks, tag tables.
  • Cell N: saves, compiles, disposes.

The benefit is that you can iterate on tag-table logic or block structure without paying the TIA Portal load penalty on each attempt. The technical cost is that the IPython kernel host has different assembly probing behaviour than python.exe, which surfaces the Contract.dll issue described next.

The Siemens.Engineering.Contract.dll FileNotFoundException

The failure that breaks Jupyter Openness is reproducible on V19 with the following minimal notebook cell:

import clr
clr.AddReference('Siemens.Engineering.dll')
from Siemens.Engineering import TiaPortal

tia = TiaPortal(attributed_assembly_path)

Running cell 1 raises:

System.IO.FileNotFoundException: Could not load file or assembly
'Siemens.Engineering.Contract, Version=19.0.0.0, Culture=neutral,
PublicKeyToken=...' or one of its dependencies.

The same code, executed through python.exe from a regular .py file, succeeds without modification. The DLL is on disk and discoverable by the Openness loader when the host is the standard CPython interpreter. The DLL is not discoverable when the host is the IPython kernel because of where and how pythonnet probes for dependent assemblies.

Diagnostic tip: copy the exact error string and grep your PublicAPI folder for the assembly name. On V19 the file is present at C:\Program Files\Siemens\Automation\Portal V19\Bin\PublicAPI\Siemens.Engineering.Contract.dll. The exception is not about presence on disk — it is about whether the CLR probing path includes that folder for transitive resolution when Siemens.Engineering.dll loads.

Root Cause: Implicit Assembly Probing Differs Between Hosts

Siemens.Engineering.dll declares a reference to Siemens.Engineering.Contract.dll in its assembly metadata. When you call clr.AddReference('Siemens.Engineering.dll'), pythonnet loads that assembly and the CLR resolver attempts to locate the contract assembly in the following order:

  1. The Global Assembly Cache (GAC).
  2. The probing path of the host application directory.
  3. Directories listed in app.config or the DEVPATH environment variable.
  4. The location of the already-loaded assembly (the PublicAPI folder).

Under python.exe pythonnet sets the load context so that the Openness PublicAPI folder is treated as a probing root, and step 4 succeeds. Under the IPython kernel, the host application directory is the Jupyter installation prefix (for example C:\Python311\Lib\site-packages\jupyter\), not the PublicAPI folder, and step 4 fails because the kernel process was not launched from PublicAPI.

The contract assembly therefore cannot be transitively resolved, and the CLR raises FileNotFoundException at the moment the JIT compiler triggers loading of the contract type referenced by TiaPortal. The exception fires from the import line because the static type initialiser touches a contract type that the loader cannot find. The error string points to Siemens.Engineering.Contract even though no source code in the notebook imports it — it is being pulled in indirectly.

Solution: Explicit clr.AddReference Before Import

The fix is to register the contract assembly explicitly before importing Siemens.Engineering. This bypasses transitive probing because pythonnet loads the contract assembly directly into the default load context.

import clr
import sys

# V19
public_api = r'C:\Program Files\Siemens\Automation\Portal V19\Bin\PublicAPI'
# V20
# public_api = r'C:\Program Files\Siemens\Automation\Portal V20\Bin\PublicAPI'

sys.path.append(public_api)
clr.AddReference('Siemens.Engineering.Contract.dll')
clr.AddReference('Siemens.Engineering.dll')

from Siemens.Engineering import TiaPortal
print('Openness load OK')

Two patterns work. The first uses absolute paths to the DLLs, which is unambiguous and version-pinned:

import clr
clr.AddReference(r'C:\Program Files\Siemens\Automation\Portal V19\Bin\PublicAPI\Siemens.Engineering.Contract.dll')
clr.AddReference(r'C:\Program Files\Siemens\Automation\Portal V19\Bin\PublicAPI\Siemens.Engineering.dll')
from Siemens.Engineering import TiaPortal

The second relies on the PublicAPI folder being on sys.path or in PYTHONPATH, which lets pythonnet locate the assembly by file name. The first pattern is recommended for production notebooks because it is explicit about which TIA Portal version is being driven, and it survives kernel restarts without relying on environment state.

Licence attribution: attributed_assembly_path in the Openness examples is a small .NET assembly compiled with the [OpennessAttributedAssembly] attribute that holds the Openness licence ticket. The attribute embedding and ticket generation pattern is covered in the Openness documentation and is also shown in the GitHub reference Maroder1/TIA-openness.

Working Code: Minimal Viable Notebook

The following cell sequence is the minimum reproducible notebook for V19 Openness in Jupyter. Execute cells top to bottom.

Cell 1 — load Openness:

import clr, sys
PUBLIC_API = r'C:\Program Files\Siemens\Automation\Portal V19\Bin\PublicAPI'
sys.path.append(PUBLIC_API)
clr.AddReference('Siemens.Engineering.Contract.dll')
clr.AddReference('Siemens.Engineering.dll')

import Siemens.Engineering as SE
from Siemens.Engineering import TiaPortal

print('Openness assembly:', SE.__file__)

Cell 2 — start TIA Portal:

ATTRIBUTED_ASSEMBLY = r'C:\Openness\MyAttributedAssembly.dll'
tia = TiaPortal(ATTRIBUTED_ASSEMBLY)
print('TIA Portal started.')

The default constructor spawns the TIA Portal UI. For headless batch work use TiaPortal(TiaPortalMode.WithoutUserInterface, ATTRIBUTED_ASSEMBLY), which does not bring up the shell and is significantly faster to load.

Cell 3 — open an existing project:

from Siemens.Engineering import ProjectInfo
PROJECT_PATH = r'C:\Work\demo.ap19'
project = tia.Projects.Open(ProjectInfo(PROJECT_PATH, PROJECT_PATH))
print('Opened:', project.Name, '| devices:', project.Devices.Count)

Cell 4 — save and clean up:

project.Save()
project.Close()
tia.Dispose()
print('Closed cleanly')

If any cell raises, the partial state is preserved and you can re-run from cell 3 without restarting the kernel because TiaPortal and Project are still alive in the namespace. That re-execution safety is the central value of Jupyter for Openness development.

Creating Blocks, Tag Tables, and Project Items

Once the TiaPortal and Project objects are alive, the standard Openness API applies identically to .py scripts and Jupyter cells. A typical extension is creating a PLC tag table and adding tags:

from Siemens.Engineering import TiaPortalMode
from Siemens.Engineering.SW.Tags import PlcTagTable

group = project.Groups.Find('Default')
plc_device = group.Devices[0]
plc_software = plc_device.Items[0].GetService[SE.SW.PlcSoftware]()
tag_table = plc_software.TagTables.Create('MyTagTable')
tag_table.Tags.Create('Motor1_Speed', SE.Hmi.Tag.PlcTagDataType.Real, '%DB1.DBX0.0')
tag_table.Tags.Create('Motor1_Run',   SE.Hmi.Tag.PlcTagDataType.Bool, '%DB1.DBX4.0')
print('Tags in table:', tag_table.Tags.Count)

For block creation, the API expects an IBlocksProvider on the PlcSoftware and an AddNewBlock signature driven by block group and type. On V19:

prog_group = plc_software.BlockGroup
fb = prog_group.Blocks.CreateInstance(
        SE.SW.Blocks.PlcBlockType.FB,
        'MotorControl',
        SE.SW.Blocks.PlcBlockGroup.Prog)
print('FB created:', fb.Name, '| number:', fb.Number)

Generating STL/SCL source into the created block is the next step and is the topic of the Maroder1/Openness_examples_python reference repository. That repository covers FBD/STL/SCL generation, HMI screen scripting, and bulk topology build-up through Jupyter notebooks. The V20 release of Openness, per published notes, includes further improvements to Python interoperability that reduce friction in exactly this kind of scripted project generation.

TIA Portal V20 Openness Improvements

V20 of TIA Portal Openness ships alongside TIA Portal V20 and, per the published release notes, focuses on improving the Python developer experience:

  • Improved Python interoperability: V20 Openness is the first version where Siemens published explicit Python support notes alongside the .NET API, including guidance for pythonnet users.
  • Improved error messages: when an assembly such as the contract DLL is missing, V20 raises an exception whose message includes the exact expected file path. V19 raises the generic FileNotFoundException shown earlier, which hides the real folder location.
  • Reduced startup latency: the WithoutUserInterface TiaPortal mode is reported as faster on V20 because background UI services are skipped earlier in the bootstrap sequence.
  • PublicAPI path changed: the V20 assembly folder is Portal V20\Bin\PublicAPI\ instead of Portal V19\Bin\PublicAPI\. Anything hard-coded to a V19 path must be updated.

For V20 the recommended notebook header is identical to V19 except for the PublicAPI path. The same explicit clr.AddReference workaround described above still applies — the contract DLL still has to be registered before the primary assembly to avoid the same probing issue under the IPython kernel.

Compatibility caveat: V20 Openness assemblies are not bit-compatible with V19. A Python notebook written for V19 will fail to load a V19 project once the Openness reference has been switched to V20 and vice versa. Pick one TIA Portal version per kernel and stick to it. Multiple Jupyter kernels, each pointing at a different PublicAPI folder, is the supported pattern for mixed-version work on the same workstation.

Troubleshooting Matrix

Symptom Host Likely cause Fix
FileNotFoundException: Siemens.Engineering.Contract at import Jupyter IPython kernel probing path excludes PublicAPI Explicit clr.AddReference(...Contract.dll) before import
FileLoadException referencing a mixed-mode assembly on Python 3.12 Any Old pythonnet build incompatible with 3.12 Upgrade to pythonnet>=3.0.3 and confirm desktop .NET runtime is installed
TiaPortal.OpennessException: licence not found Any Openness checkbox not ticked or licence ticket missing Re-run TIA Portal installer in modify mode, enable Openness; recompile attributed assembly with current ticket
COMException 0x80010105 on Project.Save() Any Background TIA Portal UI holding focus Run with TiaPortalMode.WithoutUserInterface for batch work
IOException: file locked on second Projects.Open V19 V19 file lock release can lag after Project.Close() Insert a short delay before re-open, or upgrade to V20 Openness
Kernel dies silently after tia = TiaPortal(...) Jupyter pythonnet / IPython event-loop interaction Start TiaPortal in a worker thread, or use TiaPortalMode.WithoutUserInterface
MissingMethodException on V20 with V19 sample code Any Cross-version assembly binding Pin clr.AddReference to a single TIA version per kernel
Tags appear with red dot in TIA Portal after Openness create Any Tag address outside the PLC's process image or wrong DB number Verify address against PLC hardware config; Openness does not validate address ranges
Project.Save throws on read-only project file Any Openness opens with default read/write but file ACL is restrictive Open with ProjectInfo that supplies explicit credentials, or pre-check ACLs on the project directory
Notebook hangs on tia.Dispose() Jupyter Background Openness worker threads not draining Call tia.GetType().GetMethod('Dispose', ...) after explicit project.Close(), then restart kernel if it persists

Best Practices and Performance Notes

The following rules apply when scaling Jupyter Openness from a one-off notebook to a production engineering pipeline:

  • Pin one TIA version per kernel. Install separate Jupyter kernels, one per Portal Vxx installation, and select the matching kernel in each notebook. Mixing V19 and V20 assemblies in a single process is unsupported and will raise FileLoadException or MissingMethodException at first call.
  • Use WithoutUserInterface for batch runs. The shell UI consumes a large amount of memory and licences a redundant TIA Portal window per kernel. Headless mode is the documented path for unattended automation.
  • Cache the TiaPortal instance. Re-creating it per cell is the dominant latency source. Build the instance in cell 1 and dispose only at the very end of the notebook.
  • Persist project handles explicitly. Always call project.Save() before project.Close(). Openness does not auto-save, and a notebook crash between mutations and save will leave the project on disk in the previous state.
  • Pin pythonnet. Use a requirements.txt entry pythonnet==3.0.3 or a tight upper bound for V19 and V20. Newer pythonnet drops have introduced breaking changes in the assembly load context in the past.
  • Use version-checked assembly paths. Read an environment variable or a project-level config file and resolve PublicAPI dynamically. Hard-coded V19 paths break on the first V20 rollout and are a frequent source of post-upgrade regressions.
  • Quote everything in notebooks. Markdown cells above each Openness block describing intent, expected output, and rollback. The notebook is the artefact, not the underlying Python file — another engineer reading the file later needs the rationale more than the code.
  • Use the reference repositories as starting points, not as production code. Maroder1/TIA-openness and Maroder1/Openness_examples_python exercise every major Openness surface (project, device, block, tag, HMI) through Python. Treat them as samples and adapt to your project's naming conventions and templates; do not copy verbatim into production control code without review.

The combination of TIA Portal Openness + pythonnet + Jupyter collapses what used to take a 200-line C# console app into a 30-line notebook cell, and the iteration speed of cell-by-cell execution removes the worst of the TIA Portal shell load latency. The Siemens.Engineering.Contract.dll issue is the single biggest blocker on first contact; once the explicit clr.AddReference is in place, the rest of the workflow is stable across V19 and V20, and the same pattern extends to the larger Openness API surface for blocks, tag tables, HMI screens, and bulk topology generation.

FAQ

Why does TIA Portal Openness Python work in python.exe but fail in Jupyter with Siemens.Engineering.Contract.dll not found?

The IPython kernel process is not launched from the Openness PublicAPI folder, so the CLR cannot locate Siemens.Engineering.Contract.dll through transitive probing when Siemens.Engineering.dll loads. Adding an explicit clr.AddReference to the contract DLL before importing Siemens.Engineering loads the contract into the default load context and bypasses the probing problem.

Which TIA Portal versions support Python Openness officially?

TIA Portal V19 supports Python through community-validated pythonnet configurations (3.10-3.11). TIA Portal V20 is the first version where Siemens published explicit Python support notes alongside the .NET API and includes improvements to error messages and startup behaviour for scripted workflows.

Do I need a special licence for TIA Portal Openness?

Yes. The Openness checkbox must be selected in the TIA Portal installer, and the Python script must pass an attributed .NET assembly that carries the Openness licence ticket. Without both, the TiaPortal constructor raises a licence exception on first use.

What is the minimum pip install to get started with TIA Openness in Jupyter?

Install pythonnet>=3.0.3 and jupyterlab (with ipykernel) into a 64-bit Python 3.11 or 3.12 environment, then verify the .NET bridge with import clr; clr.AddReference('System.Windows.Forms') before touching any Siemens assemblies.

Can one Jupyter kernel drive both V19 and V20 Openness?

No. The assemblies are not bit-compatible and mixing them in a single process causes MissingMethodException or FileLoadException. Use one Jupyter kernel per TIA Portal version, each with the matching PublicAPI path in its kernel configuration or notebook header.

Back to blog