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:
- They're writing to a commandable analog property (Analog Value, Analog Output, Setpoint) and bacpypes3 raised a type error like "expected Real, got float" or the reverse. They want to understand whether a plain Python float is acceptable or whether they have to wrap it.
- They're building a custom application that subclasses a BACnet object — usually because they want to add a non-standard property or override the encoding — and they need to know what the parent type contributes before they extend it.
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.
- float — The CPython numeric base. It gives
Realinstances all the operators and conversions you expect:Real(2.0) + 1.5returns a number,float(Real(2.0))returns2.0,repr()formats normally, hashing and equality work against other numerics. Subclassingfloatmeans aRealis, for every practical purpose, a Python float that happens to also know how to encode itself. - Atomic — The bacpypes3 mixin that marks a type as a BACnet application primitive. Atomic supplies the metadata bacpypes3 needs to encode and tag the value: the application tag number, the class-side methods used when building APDUs, and the type-identity hooks the library uses when it dispatches generic
encode/decodework. Other primitives in the package follow the same pattern —Boolean(Atomic, int),Unsigned(Atomic, int),Double(Atomic, float),CharacterString(Atomic, str), and so on.
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
- Confusing REAL with DOUBLE. Python's
floatis double-precision; BACnet REAL is single. If you write a value that has more precision than a 32-bit float can hold — e.g.0.1, certain calibration constants, or sensor values stored in scientific instruments — you will see a small rounding difference between what you wrote and what the device reports back. The fix is to useDoubleonly when the property's datatype is genuinely DOUBLE; otherwise accept that REAL has roughly 7 significant decimal digits of precision and design around it. - Passing a string by mistake.
Real("72.5")behaves likefloat("72.5")— the parse works, you get72.5. ButReal("72.5 degF")raises aValueErrorthe same wayfloat()would. Input that arrives from a UI, CSV, or REST payload should be parsed and validated before you hand it to the primitive constructor, not after. - NaN and infinity. A Python float happily holds
float("nan")andfloat("inf"); those values encode into IEEE 754 just fine but many BACnet devices reject them at the application layer, and some BAS front-ends display them as blanks or alarms. If you ever build aRealfrom a calculation result, guard against NaN and infinity at the boundary. - Comparing a round-tripped Real to the original Python float and expecting bit-equality. Because
Realinherits from CPython's 64-bitfloat, simply constructingReal(0.1)does not narrow the in-memory value —Real(0.1) == 0.1isTrue. The single-precision truncation happens at BACnet wire encoding/decoding time. After a WriteProperty/ReadProperty round-trip, the value you read back has been truncated to 32-bit single-precision; comparing it bit-equal to the original Python double will often beFalse. Usemath.isclosefor round-tripped comparisons, not==. - Subclassing Real to add behavior. Because
Realinherits fromfloat— an immutable built-in — you cannot reliably override__init__in a subclass to add state. Use__new__instead, and remember that any per-instance attributes must be set up through the float construction path. Most users do not need to subclass; they only think they do.
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
PYThe __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:
- Round-trip values that differ by more than single-precision rounding would explain. That is a device or driver issue, not a Real-vs-float issue — capture the WriteProperty and the subsequent ReadProperty in Wireshark and verify the on-the-wire values against your source.
- A vendor stack that rejects encoded REALs from bacpypes3 but accepts them from another tool. File an issue against the upstream bacpypes3 project on PyPI / GitHub with a reproducible capture. The community is small but responsive.
- Property writes that appear to succeed but never change
Present_Value. Almost always a priority-array problem rather than an encoding problem — see our BACnet write priority array explained entry before re-reading the source.
Source Attribution
- ASHRAE Standard 135 — BACnet—A Data Communication Protocol for Building Automation and Control Networks. The normative reference for the REAL application primitive (clause 20) and the property datatypes that use it.
- bacpypes3 on PyPI — Current release, install instructions, and links to the project repository. Check the version installed in your environment before relying on any specific API signature.
- bacpypes3 documentation — Project documentation for primitives, application classes, and the async API.
- Python
floatreference — CPython documentation for the built-in float type that bacpypes3 inherits from.
Additional validation and field usage notes 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.