bacpypes3 Real Class: Why It Inherits From Atomic and float

Tools & Librariesbacpypes3RealAtomicprimitivedata
May 26, 2026|8 min read

In bacpypes3, class Real(Atomic, float) is the Python representation of the BACnet REAL primitive (a 4-octet IEEE 754 single-precision value defined in ASHRAE 135 clause 20.2.6). The float base gives the class normal Python numeric behavior — arithmetic, comparison, repr() — while the Atomic base supplies the BACnet encoding hooks (tag class, tag number, application tag, and the encode/decode helpers) that let the value round-trip on the wire. You construct a Real the same way you construct a Python float (Real(72.5)), and you pass it wherever bacpypes3 expects a primitive REAL value — most commonly the value argument of a WriteProperty request on a property whose datatype is REAL.

Why You're Reading the bacpypes3 Source

The query that brought you here — class Real(Atomic, float) — is the literal class declaration you see when you grep the bacpypes3 package for the BACnet REAL primitive. People land on that line for two reasons:

Both cases hinge on the same underlying design choice: bacpypes3 represents every BACnet primitive as a dual-inheritance class that is both a Python value type and a BACnet-aware encoder. Understanding why answers most of the practical questions.

What ASHRAE 135 Says About REAL

ASHRAE Standard 135 defines REAL as one of the application primitive data types. The on-the-wire form is a 4-octet IEEE 754 single-precision floating-point value with an application tag of 4. Anywhere the standard refers to a property whose datatype is REAL — Present_Value on an Analog Input, COV_Increment, High_Limit, Resolution, and many others — the encoded property value must be that 4-octet IEEE 754 number with the correct tag.

The standard is the authority here. Python's built-in float is 64-bit (IEEE 754 double-precision), which is wider than BACnet REAL. That width mismatch is the seed of every gotcha discussed below.

What Each Base Class Contributes

The dual inheritance is not an accident; each base is doing specific work.

The order matters. Atomic comes first because Python's MRO walks left-to-right; methods defined on Atomic (or any of its mixins) take precedence over the plain-float fall-through. This is how bacpypes3 can hook the construction path to validate range and the encoding path to emit the correct tag without breaking arithmetic.

Where the Class Lives

In bacpypes3, primitive application data types live in bacpypes3.primitivedata. The import is straightforward:

from bacpypes3.primitivedata import Real

x = Real(72.5)
print(float(x))       # 72.5  — works because Real is-a float
print(x + 1.0)        # 73.5  — arithmetic flows through the float base
print(type(x))        # <class 'bacpypes3.primitivedata.Real'>

The same module is where you find Boolean, Unsigned, Integer, Double, OctetString, CharacterString, BitString, Enumerated, Date, Time, and ObjectIdentifier. If you ever need to see the canonical Python representation of any BACnet primitive, that module is the right place to start. For other constructed types (lists of values, schedules, weekly schedules), see our companion entry on bacpypes3 DailySchedule, TimeValue, AnyAtomic source and usage.

Using Real in a WriteProperty Call

The most common reason to construct a Real directly is to write to a commandable analog property. The bacpypes3 high-level API will usually accept a plain Python float and coerce it for you, but being explicit removes ambiguity — especially when the property's datatype is genuinely REAL and not the wider DOUBLE. Patterns look roughly like this:

from bacpypes3.primitivedata import Real, ObjectIdentifier
# ... inside an Application subclass with an established device ...

setpoint_value = Real(68.0)
await self.write_property(
    device_address,                       # e.g. Address("10.0.5.42")
    ObjectIdentifier("analog-value,1"),   # target object
    "present-value",                       # property identifier
    setpoint_value,                        # value
    priority=8,                           # optional, for commandable objects
)

The exact method signature and helper class names evolve across bacpypes3 releases. Pin against the version you have installed and cross-check with the project README on PyPI rather than copying a snippet wholesale. For a more guided tour of the bacpypes3 application loop, see BACpypes3: custom BACnet applications in Python.

Common Pitfalls

Confirming the Class Locally

If you want to see the declaration in your installed copy of bacpypes3 rather than trusting any external write-up, the standard Python tools will show it:

# Show where the class is defined and (if available) print its source
python - <<'PY'
import inspect, bacpypes3.primitivedata as pd
print(inspect.getfile(pd.Real))
print(pd.Real.__mro__)
try:
    print(inspect.getsource(pd.Real))
except (OSError, TypeError):
    pass
PY

The __mro__ printout will show the full method resolution order — the leftmost entries are the bacpypes3 mixins, then float, then object. That is the cleanest way to confirm that the class on your machine matches the layout this entry describes, and it sidesteps any uncertainty about which branch or release you happen to have installed.

When to Escalate

Most type-mismatch and precision questions resolve once you understand the dual inheritance. Escalate beyond a quick fix when you see:

Source Attribution

Additional validation and field usage notes by SiteConduit.

bacpypes3RealAtomicprimitivedataBACnet types

Was this article helpful?

Related Articles

Need to do this remotely? SiteConduit provides Layer 2 access that preserves BACnet broadcasts — no BBMD needed for remote sessions. Join the waitlist.

SC

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.