In bacpypes3, Choice is the Python base class that models the ASHRAE 135 CHOICE constructed data type — an alternative-of-named-elements, where exactly one named element is set at a time. The _elements class attribute declares the allowed alternatives (a name-to-type mapping), and _choice is the per-instance attribute that records which element name is currently set. The canonical source lives in the bacpypes3 constructed-data module; the easiest way to find the current location is to import it from the package and read its __module__ at runtime, or browse the GitHub repository directly. See JoelBender/BACpypes3 on GitHub for the authoritative implementation.
What You're Looking At
If you've been reading bacpypes3 source — or staring at a traceback from a Choice subclass — you've probably seen three things together: a class that inherits from Choice, a class-level _elements declaration listing alternative element names and their types, and an instance-level _choice that tells you which element is currently set. The search query “bacpypes3 choice _choice _elements source” almost always comes from one of three situations:
- You're building a custom BACnet service or notification payload that nests a CHOICE field, and the documentation example stops short of the construction call.
- You're reading the bacpypes3 source for a stock type (a notification parameters union, a date-or-datetime field, a property value alternation) and trying to map the Python declaration back to the ASHRAE 135 ASN.1 production.
- You hit a runtime error along the lines of “exactly one element must be set” or a type-mismatch on encode, and you need to know which attribute the library is checking.
The rest of this entry covers the concept (what CHOICE is in BACnet), the pattern (how bacpypes3 models it), how to find the current source, and the encoding pitfalls that produce the most common errors.
CHOICE in ASHRAE 135 — the Concept
BACnet's data types are defined in ASHRAE Standard 135 using ASN.1 notation. CHOICE is one of the standard constructed types: it declares a set of named alternatives, and any given instance of the type carries exactly one of those alternatives. ASN.1 CHOICE is structurally similar to a tagged union in other languages — discriminated at runtime by which element is present, not by an explicit type code.
Every CHOICE production in 135 has the same shape:
- A name (e.g., BACnetTimeStamp, BACnetDateTime, BACnetNotificationParameters).
- An ordered list of element name : type pairs.
- Per-element context tags that distinguish the alternatives on the wire.
On encode, exactly one element is selected and tagged. On decode, the receiver inspects the context tag to know which alternative arrived. If more than one element is present in memory at encode time, the type has been used incorrectly — that is the constraint bacpypes3 enforces with the “exactly one” check.
How bacpypes3 Models a CHOICE Type
bacpypes3 maps the ASN.1 CHOICE production onto a Python class hierarchy. A concrete BACnet CHOICE type is declared as a subclass of Choice, with a class-level _elements mapping the element names from the 135 production to the Python types they carry. Element types are themselves bacpypes3 classes — atomics like Real and Unsigned, primitive containers, or further constructed types like Sequence.
When you instantiate the subclass, the active alternative is recorded as _choice (the element name as declared in _elements), and the value itself is stored as an attribute by that same name. The class enforces the invariant that exactly one element name is set per instance — assigning two of them is what raises the runtime error referenced above.
For an end-to-end walkthrough of building custom services on top of these base classes, see BACpypes3: Custom BACnet Applications in Python. For background on how the primitive types fit into the same hierarchy (and why a class can inherit from both Atomic and a Python built-in), see bacpypes3 Real Class: Why It Inherits From Atomic and float.
Finding the Source for Choice, _choice, and _elements
Module layout in bacpypes3 has changed across early releases, and pinning a path in writing tends to age badly. Use one of these version-independent approaches instead:
- Import and introspect. Open a Python REPL with bacpypes3 installed and run:
# Locate the Choice base class in the installed version
import bacpypes3
from bacpypes3 import constructeddata # may also be re-exported elsewhere
print(constructeddata.Choice.__module__)
print(constructeddata.Choice.__qualname__)
# Inspect a stock CHOICE type to see _elements in action
from bacpypes3 import basetypes
ts = basetypes.TimeStamp # example: an ASHRAE 135 CHOICE
print(ts.__mro__)
print(ts._elements)- Read the repo on GitHub. The canonical source is at github.com/JoelBender/BACpypes3. Search the repository for
class Choicefor the base class, and for_elements =to see every stock CHOICE type the library ships. - Check the wheel you actually installed. Versions on PyPI may differ from the latest GitHub commit. After
pip install bacpypes3, runpip show -f bacpypes3to see the file list, or open the package directory under your virtualenv'ssite-packages. TheChoiceclass definition is small enough to read in one screen.
Avoid copy-pasting a module path from a Stack Overflow answer without verifying it against your installed version — the reorganization between bacpypes (the original) and bacpypes3 (the asyncio rewrite) moved several symbols, and not every blog post has kept up.
Construction and Common Pitfalls
Two patterns cover most CHOICE construction in user code:
- Keyword form. Pass the element name as a keyword argument to the constructor:
BACnetTimeStamp(time=...). The keyword must match a name declared in_elements, and the value must be an instance of (or convertible to) the declared type. - Assign after construction. Construct an empty instance and assign the chosen element by attribute name. Reassigning a different element name on the same instance after one is set is the canonical way to trip the “exactly one” invariant — clear or rebuild the instance instead.
Pitfalls to watch for:
- Passing a raw Python value where a bacpypes3 type is required. Some elements accept a primitive (an
intcoerced into anUnsigned); others require the wrapped type. When in doubt, wrap explicitly. - Setting more than one element. If your construction logic branches on input, make sure each branch produces a fully formed CHOICE with exactly one element set. Resetting an attribute to
Noneis not the same as clearing the chosen element on every version of the library — rebuild the instance. - Mistaking CHOICE for Sequence.
Sequencemeans “all named fields, in order.”Choicemeans “exactly one of these named fields.” If you find yourself populating every element of an_elementsdeclaration, you're probably reaching forSequence, notChoice. - Subclassing Choice for a vendor extension without context tags. Custom CHOICE types must still produce a wire-format encoding that an 135-compliant peer can decode. Element ordering and context tags matter — read the existing
_elementsdeclarations inbasetypesfor examples before inventing your own.
When to Escalate
If you've confirmed the _elements declaration matches the ASHRAE 135 production, the active element is correctly set, and you still see an encoding or decoding failure, the issue is usually one of three things: a context-tag mismatch in a custom subclass, a library version that predates a fix you need, or a peer device that does not implement the CHOICE alternation correctly. File issues against bacpypes3 with a minimal reproducer at the project issue tracker. For peer-device problems, capture the exchange on the wire and verify the context tags on each alternative against the relevant 135 clause.
Source Attribution
The technical guidance in this entry is informed by the following sources:
- JoelBender/BACpypes3 — Source repository for the bacpypes3 library, including the
Choicebase class and the stock CHOICE type declarations in the constructed-data and base-types modules. - bacpypes3 on PyPI — Current release artifact and changelog. Use
pip showto verify the version actually installed in your environment before pinning any module path. - ASHRAE Standard 135 — BACnet—A Data Communication Protocol for Building Automation and Control Networks. Normative source for the CHOICE constructed data type, the ASN.1 productions that define each stock CHOICE, and the context-tag rules that govern wire encoding.
- ITU-T X.680 (ASN.1 specification) — Defines the CHOICE production that ASHRAE 135 uses. Useful when you need to understand why exactly-one-element-set is a structural property of the type, not an implementation choice.
Additional testing and field validation by SiteConduit.
Was this article helpful?
Related Articles
Why bacpypes3 Defines class TimeValue(Sequence)
Tools & Librariesbacpypes3 class Any: The Open BACnet Value Container
Tools & Librariesbacpypes3 Time Class: Why It's a Tuple of (Hour, Minute, Second, Hundredth)
Niagara / TridiumTridium Upgrade: Niagara Version Upgrade and Migration Guide
Need to do this remotely? SiteConduit provides Layer 2 access that preserves BACnet broadcasts — no BBMD needed for remote sessions. Join the waitlist.
SiteConduit Technical Team
Idea Networks Inc.
SiteConduit builds managed remote access for building automation. Our knowledge base is maintained by BAS professionals with hands-on experience deploying and troubleshooting BACnet, Niagara, Modbus, and Facility Explorer systems.