To compare float numbers in Jinja2, cast both sides with the float filter before using >, < or ==, because values that arrive from YAML, environment variables, form input or Ansible extra vars are very often strings, and Jinja2 compares strings lexically rather than numerically. For equality checks, compare the absolute difference against a small tolerance instead of using == directly, exactly as you would in Python.
{% if value | float > 1.5 %} for ordering, and {% if (a | float - b | float) | abs < 0.0001 %} for equality. Add a default, value | float(0), when the variable might be empty or malformed. Never rely on version_compare for floats.This article covers the string versus number trap, how the float and round filters behave, tolerance comparisons, the differences between plain Jinja2 and Ansible’s Jinja2, and how to format the result once you have it. If you first need to confirm a variable is actually set before comparing it, the guide on checking whether a variable exists in Jinja2 pairs well with this one.
Why float comparisons go wrong in Jinja2
Jinja2 is a Python templating engine, and it inherits Python’s comparison semantics. In Python, "10.5" > "9.2" is False, because string comparison looks at the first character and "1" sorts before "9". Jinja2 does exactly the same thing when both operands are strings. When one operand is a string and the other is a number, Python 3 raises a TypeError, and Jinja2 surfaces that as a template rendering error.
The reason this bites people is that most data reaching a template is text. A few common sources:
- Environment variables are always strings.
os.environ["THRESHOLD"]gives you"0.75", not0.75. - Ansible extra vars passed with
-e key=valueon the command line are strings unless you use JSON or YAML syntax. - Inventory host variables in INI format inventories are strings.
- Command output captured with
registeris a string instdout. - Web form input in Flask or Django is a string.
- YAML values with quotes such as
ratio: "1.5"are strings. Unquotedratio: 1.5is a float.
Here is a minimal reproduction using plain Jinja2 in Python:
from jinja2 import Template
t = Template("{{ 'yes' if load > threshold else 'no' }}")
print(t.render(load="10.5", threshold="9.2")) # prints: no (string comparison)
print(t.render(load=10.5, threshold=9.2)) # prints: yes (numeric comparison)
t2 = Template("{{ 'yes' if load > threshold else 'no' }}")
print(t2.render(load="10.5", threshold=9.2)) # TypeError: '>' not supported between str and floatThe float filter
The fix is the built in float filter. It converts the value with Python’s float() and, crucially, it does not raise on bad input. If the conversion fails it returns a default, which is 0.0 unless you pass another value.
{# Both sides cast, works whether the inputs are strings or numbers #}
{% if load | float > threshold | float %}
High load
{% endif %}
{# Explicit default when the variable may be empty or garbage #}
{% if cpu_pct | float(0) >= 90 %}
Alert
{% endif %}
{# Filter binds tighter than the comparison operator, so this is (a|float) > (b|float) #}
{{ a | float > b | float }}Two things to keep in mind. First, float applied to an undefined variable follows the same rule as any other filter: with the default Undefined class it converts to the default value, but with StrictUndefined (which Ansible uses) it raises. Guard with default first when the variable may not exist: {{ x | default(0) | float }}. Second, the filter is silent about failure. "abc" | float quietly becomes 0.0, so if 0 is a meaningful value in your logic, validate the input elsewhere or use a sentinel default such as float(-1).
set_fact: threshold_f: "{{ threshold | float }}" once and compare the fact afterward. In Flask, convert in the view and pass a real float into render_template.Equality: compare with a tolerance
Even when both sides are genuine floats, == is unreliable for values that came out of arithmetic. 0.1 + 0.2 is not exactly 0.3 in IEEE 754 binary floating point, and Jinja2 will happily tell you so. The standard answer is to check whether the absolute difference is below a tolerance you choose based on the precision your data actually has.
{% set a = 0.1 + 0.2 %}
{% set b = 0.3 %}
{{ a == b }} {# False #}
{{ (a - b) | abs < 0.000001 }} {# True #}
{# Reusable pattern with string inputs #}
{% set eps = 0.0001 %}
{% if (measured | float - expected | float) | abs < eps %}
Within spec
{% else %}
Out of spec by {{ (measured | float - expected | float) | abs }}
{% endif %}Jinja2 ships abs as a built in filter, so nothing extra is needed. If you prefer relative tolerance for numbers that span several orders of magnitude, express it in the template: (a - b) | abs <= rel * ([a | abs, b | abs] | max). The max filter accepts a list, which is why the two values are wrapped in brackets.
Rounding as a comparison tool
A second way to sidestep float noise is to round both sides to the precision you care about and compare the results. The round filter takes the number of decimal places and a method: common (the default, rounds half up), ceil or floor.
{{ 3.14159 | round(2) }} {# 3.14 #}
{{ 3.14159 | round(2, 'ceil') }} {# 3.15 #}
{{ 42.55 | round }} {# 43.0, still a float #}
{{ 42.55 | round | int }} {# 43, now an integer #}
{% if price | float | round(2) == target | float | round(2) %}
Prices match to the cent
{% endif %}Note that round returns a float, not an integer, so 42.55 | round renders as 43.0. Chain | int if you want a whole number. Also note that the common method calls Python’s own round(), which rounds ties to the nearest even digit and is subject to the usual binary representation quirks, so values sitting exactly on a half may not round the way a spreadsheet would. If that matters, round both sides the same way and compare, and the quirk cancels out.
Ansible specifics
Ansible uses Jinja2 for every templated value, but with two twists that matter here. The first is that until a task actually evaluates the expression, everything in a playbook is text, and Ansible then attempts to convert results back to native types. The second is that Ansible exposes extra filters and tests, and one of them, version_compare (aliased as the version test), is regularly misused for floats.
version_compare is not a float comparison
version compares dotted version strings segment by segment. Under that rule "1.10" is greater than "1.9", which is correct for versions and wrong for decimals. Use it for software versions only:
# Correct: version strings
- name: Require a recent kernel
ansible.builtin.assert:
that: ansible_kernel is version('5.15', '>=')
# Wrong for decimals: '1.10' is version 1.10, not one point one
- ansible.builtin.debug:
msg: "{{ '1.10' is version('1.9', '>') }}" # True, because it is a version test
# Correct for decimals
- ansible.builtin.debug:
msg: "{{ '1.10' | float > '1.9' | float }}" # False, 1.1 is less than 1.9Typical playbook pattern
- name: Read disk usage percentage
ansible.builtin.shell: df --output=pcent / | tail -1 | tr -dc '0-9.'
register: disk_pct
changed_when: false
- name: Store as a float once
ansible.builtin.set_fact:
disk_pct_f: "{{ disk_pct.stdout | float(0) }}"
- name: Fail when usage is above the limit
ansible.builtin.fail:
msg: "Disk at {{ disk_pct_f }}%, limit is {{ disk_limit }}%"
when: disk_pct_f | float > disk_limit | floatNotice the second | float in the when clause even though the fact was already cast. A set_fact value goes through templating and may come back as a string depending on the Ansible version and whether native Jinja2 types are enabled, so casting again in the comparison costs nothing and removes the ambiguity. The Ansible tests documentation describes the version test and its operators.
Comparison approaches side by side
| Goal | Expression | Notes |
|---|---|---|
| Greater or less than | a | float > b | float | Safe for strings and numbers alike |
| Equality with tolerance | (a | float - b | float) | abs < 0.0001 | Pick the tolerance from your data’s precision |
| Equality to N decimals | a | float | round(2) == b | float | round(2) | Readable for money and percentages |
| Missing or empty input | x | default(0) | float | Required under StrictUndefined (Ansible) |
| Software versions | v is version('2.10', '>=') | Ansible only, never for decimals |
| Range check | 0.0 <= x | float <= 1.0 | Chained comparisons work as in Python |
Formatting the result
Once a comparison passes, you usually want to print the number. Jinja2 supports Python’s format mini language through the format filter and through the % operator, and both accept float precision specifiers.
{{ "%.2f" | format(ratio | float) }} {# 0.75 #}
{{ "%.1f%%" | format(pct | float) }} {# 87.5% #}
{{ "{:,.2f}".format(total | float) }} {# 12,345.68 with thousands separator #}
{{ ratio | float | round(3) }} {# 0.746 #}Formatting is also a cheap way to see what type you are dealing with. If {{ x }} renders 1.5 but {{ x | float | round(1) }} renders something different, the value was a string containing whitespace or a stray character. For messier inputs, the article on regular expressions in Jinja2 shows how to strip everything except digits and the decimal point before casting. And if you want to wrap the whole comparison in a reusable block, see how to wrap a block in Jinja2 for macro and call block patterns.
"1,5" (comma decimal separator) convert to 0.0 with the float filter. Replace the comma first: {{ x | replace(',', '.') | float }}.Troubleshooting
TypeError: ‘>’ not supported between instances of ‘str’ and ‘float’
One side is a string. Apply | float to both operands. In Ansible, the offending value is almost always an extra var, a registered stdout, or an INI inventory variable.
The comparison silently picks the wrong branch
Both sides are strings, so lexical comparison ran without an error. "10" < "9" is the classic symptom. Same fix: cast both sides.
Value is always 0 after the float filter
The input was not parseable, so the filter returned its default. Print the raw value with {{ x | pprint }} to see hidden whitespace, quotes, units such as % or MB, or a comma decimal separator, then clean it with trim, replace or regex_replace before casting.
Equality passes in Python but fails in the template
You are comparing a float to a string representation, for example 1.5 == "1.5", which is False in Python and therefore in Jinja2. Cast the string side, or better, compare with a tolerance.
Ansible says the fact is a string even after set_fact with float
Templated values are rendered to text unless native Jinja2 types are in effect. Cast again in the comparison (fact | float) rather than trusting the stored type. That pattern works on every Ansible version.
Frequently asked questions
Does Jinja2 have a built in float type check?
Jinja2 provides the number test, which is true for integers and floats, and the float and integer tests for the specific types. Ansible adds these too. Use {% if x is float %} to branch on type, but remember a numeric string still fails that test.
Why does 0.1 + 0.2 == 0.3 return False in a template?
Because Jinja2 evaluates arithmetic with Python floats, and those are binary IEEE 754 values that cannot represent 0.1 or 0.2 exactly. The sum lands a tiny fraction above 0.3. Compare the absolute difference to a small tolerance, or round both values to the precision you care about.
Can I compare a float to an integer in Jinja2?
Yes. Python compares ints and floats numerically, so 1.0 == 1 is true and 2.5 > 2 is true. The only problem case is when either operand is a string. Cast strings with the float or int filter and the comparison behaves as expected.
What is the difference between float and int filters for comparisons?
int truncates toward zero and turns "2.9" into 2, which changes the outcome of a comparison. Use float whenever the data can carry a fractional part, and reserve int for counts and identifiers. Both accept a default argument for unparseable input.
Is version_compare ever appropriate for numbers?
Only for dotted version strings such as 2.10.1, where each segment is an independent integer. For decimals it produces wrong answers, treating 1.10 as larger than 1.9. For ordinary floats, always cast with the float filter and use standard comparison operators.
The bottom line
Float comparison in Jinja2 is Python float comparison with one extra hazard: your inputs are usually strings. Cast both sides with | float, give it a default when the variable may be empty, and the ordering operators work exactly as you expect.
For equality, skip == and compare the absolute difference to a tolerance, or round both sides to a fixed number of decimals. Keep version_compare for version strings, and do your casting once at the boundary so the rest of the template stays readable.

