Skip to main content

Expressions and Operators

An expression computes a value. This page covers the operators, conditional expressions, string formatting, indexing and slicing, comprehensions, function calls, and lambda expressions that the Atmos Automation Language provides, and how they group when combined.

Arithmetic​

OperatorMeaningExampleResult
+Addition; concatenation for strings, bytes, lists, and tuples1 + 23
-Subtraction5 - 7-2
*Multiplication; repetition for strings, lists, and tuples"ab" * 2"abab"
/True division, always a float7 / 23.5
//Floor division-7 // 2-4
%Remainder; string formatting when the left side is a string-7 % 32
-x, +xNegation and identity-(2 + 3)-5

Floor division rounds toward negative infinity, and the remainder takes the sign of the right operand, so a == (a // b) * b + a % b always holds:

print(7 // 2, -7 // 2, 7 // -2) # 3 -4 -4
print(7 % 3, -7 % 3, 7 % -3) # 1 2 -2
print(7.5 // 2, 7.5 % 2) # 3.0 1.5
print(7 / 2, 6 / 3) # 3.5 2.0

Mixing an integer and a float produces a float. Division or remainder by zero is an error. Both operands must have compatible types: "a" + 1 and [1] + (2,) are errors, so convert first with str() or list().

The Atmos Automation Language has no exponent operator. For a nonnegative integer exponent, multiply in a loop:

def power(base, exponent):
result = 1
for _ in range(exponent):
result = result * base
return result

print(power(2, 10)) # 1024
print(sum([1, 2, 3])) # 6

Bitwise operators​

Integers support & (and), | (or), ^ (xor), ~ (complement), <<, and >>. A negative shift count is an error, and a left shift count must be below 512.

print(5 & 3, 5 | 3, 5 ^ 3) # 1 7 6
print(~5) # -6
print(1 << 10, 1024 >> 3) # 1024 128

Applied to dictionaries and sets, | also merges and unions; see Data Types.

Comparison​

The operators ==, !=, <, >, <=, and >= return True or False. Equality compares by value across lists, tuples, dictionaries, and sets, and integers equal floats with the same value (1 == 1.0). Ordering is defined for numbers, strings, bytes, and sequences of comparable elements, and comparing values of unrelated types, such as 1 < "a", is an error.

Comparisons do not chain. Python's 0 < x < 10 is a syntax error here (< does not associate with <); combine two comparisons with and, or add parentheses:

x = 5
print(0 < x and x < 10) # True
print((1 < 2) == True) # True

Membership​

The operators in and not in test whether a value occurs in a collection:

Collectionx in collection checks
stringx is a substring (the left side must be a string)
list, tuple, range, setx equals an element
dictionaryx is a key
bytesx is a subsequence of bytes, or an integer byte value
print("an" in "banana") # True
print(3 in [1, 2, 3]) # True
print("region" in {"region": 1}) # True
print(1 in {"a": 1}) # False
print("x" not in ["a", "b"]) # True

Boolean operators​

The and, or, and not operators accept any value, using the truthiness rules. The binary operators short-circuit and return one of their operands rather than a plain boolean:

  • The expression a or b returns a if a is true, otherwise b.
  • The expression a and b returns a if a is false, otherwise b.
  • The expression not a always returns True or False.
print(0 or "default") # default
print("set" or "default") # set
print([] and "never") # []
print(1 and 2 and 3) # 3
print(not []) # True

Use or to supply a fallback: name = ctx.flags.get("name") or "world". The right operand is evaluated only when needed, so d and d["key"] is safe when d may be empty.

Conditional expressions​

A conditional expression, written value_if_true if condition else value_if_false, selects between two values. Only the chosen branch is evaluated. Because top-level names can be assigned once, a conditional expression is the usual way to choose a value for a top-level assignment.

replicas = 5
size = "large" if replicas > 3 else "small"
print(size) # large

grade = "high" if replicas > 10 else "mid" if replicas > 3 else "low"
print(grade) # mid

String formatting​

Percent operator​

The percent operator, written format_string % values, fills conversions that begin with %. A single value supplies one conversion, and a tuple supplies several. A dictionary supplies named conversions such as %(name)s.

ConversionResult
%sstr() of the value
%rrepr() of the value
%d, %iInteger; a float is truncated, and a string or boolean is an error
%o, %x, %XInteger in octal or lowercase or uppercase hexadecimal
%e, %EFloat in exponent notation, such as 1.234500e+03
%fFloat with six decimal places
%g, %GFloat in the shortest form
%cA character from an integer code point, or a one-character string
%%A literal percent sign
print("%s has %d replicas" % ("api", 3)) # api has 3 replicas
print("%r" % "quoted") # "quoted"
print("%x and %o" % (255, 8)) # ff and 10
print("%f" % 1.5) # 1.500000
print("%(svc)s in %(env)s" % {"svc": "api", "env": "prod"}) # api in prod
print("%d%%" % 50) # 50%
print("%s" % ([1, 2],)) # [1, 2]

Flags, field widths, and precision (%5d, %-8s, %.2f, %05d) are not supported and produce an error such as unknown conversion %5. To pad a string, build the padding explicitly:

label = "api"
print(label + " " * (8 - len(label)) + "|") # api |

A tuple is always read as the list of values, so wrap a tuple you want to print in a one-element tuple, as in the last example above. The number of values must match the number of conversions.

The format method​

The method call "template".format(...) replaces {} placeholders. Placeholders can be automatic ({}), numbered ({0}), or named ({name}), and !r or !s selects repr() or str(). Write {{ and }} for literal braces.

print("{} is {}".format("api", "up")) # api is up
print("{0}-{1}-{0}".format("a", "b")) # a-b-a
print("{svc}:{port}".format(svc="api", port=8080)) # api:8080
print("{!r}".format("x")) # "x"
print("{{literal}} {}".format(1)) # {literal} 1

Format specifications such as {:>8} or {:.2f}, attribute access such as {x.name}, and indexing such as {items[0]} are not supported. Do the work before formatting, for example "{}".format(x.name). Mixing automatic and numbered placeholders in one template is an error, and a missing argument is an error. Extra positional arguments are ignored.

Indexing and slicing​

The expression sequence[i] reads one element of a string, bytes, list, tuple, or range. Indexes start at 0, and a negative index counts from the end. An index outside the sequence is an error. Likewise, dictionary[key] reads a value and is an error for a missing key.

items = ["a", "b", "c", "d"]
print(items[0], items[-1]) # a d
print({"k": 1}["k"]) # 1

A slice sequence[start:stop:step] returns a new value of the same type. Each part is optional. Slice bounds are clamped, so an out-of-range slice produces a shorter or empty result instead of an error. A step of zero is an error, and a negative step walks backward.

items = ["a", "b", "c", "d", "e"]
print(items[1:3]) # ["b", "c"]
print(items[:2]) # ["a", "b"]
print(items[-2:]) # ["d", "e"]
print(items[::2]) # ["a", "c", "e"]
print(items[::-1]) # ["e", "d", "c", "b", "a"]
print(items[2:100]) # ["c", "d", "e"]
print("hello"[1:4]) # ell

Assignment through an index, items[i] = value or d[key] = value, works on lists and dictionaries. Slice assignment is not available; build a new list with concatenation instead. See Statements.

Attribute access and calls​

The expression value.name reads an attribute or a method. A method is looked up on the value's type, so "abc".upper is a function bound to the string, and calling it with parentheses runs it. Reading an attribute that does not exist is an error.

Call a function with positional arguments, keyword arguments, or both. A * before an iterable expands it into positional arguments, and ** before a dictionary expands it into keyword arguments:

def deploy(name, region, replicas = 1):
return "{} in {} x{}".format(name, region, replicas)

args = ["api", "us-east-1"]
options = {"replicas": 3}
print(deploy("api", "us-east-1")) # api in us-east-1 x1
print(deploy(name = "api", region = "eu-west-1")) # api in eu-west-1 x1
print(deploy(*args, **options)) # api in us-east-1 x3

In a call, write positional arguments first, then keyword arguments, then * and ** expansions. A positional argument after *args is a syntax error. Supplying the same parameter twice, an unknown keyword, or too few or too many arguments is an error. See Statements for how parameters are declared.

Comprehensions​

A comprehension builds a list or dictionary from an iterable in one expression. It reads [expression for variable in iterable], with optional if filters and additional for clauses.

numbers = [1, 2, 3, 4, 5, 6]
print([n * n for n in numbers if n % 2 == 0]) # [4, 16, 36]
print({n: n * n for n in numbers if n < 4}) # {1: 1, 2: 4, 3: 9}
print([(x, y) for x in [1, 2] for y in ["a", "b"]])
# [(1, "a"), (1, "b"), (2, "a"), (2, "b")]

The loop variables can unpack pairs, which is the usual way to transform a dictionary:

services = {"api": 3, "worker": 5}
print([name for name, count in services.items() if count > 3]) # ["worker"]
print({name: count * 2 for name, count in services.items()}) # {"api": 6, "worker": 10}

The variables of a comprehension exist only inside it; a name with the same spelling outside is untouched. Python's set comprehensions and generator expressions have no equivalent here. Wrap a list comprehension in set(...) to build a set, and pass a list comprehension to any() or all().

print(set([n % 3 for n in range(10)])) # set([0, 1, 2])

Lambda expressions​

A lambda expression, written lambda parameters: expression, creates an anonymous function whose body is a single expression. Parameters follow the same rules as def, including defaults, *args, and **kwargs.

double = lambda x: x * 2
pick = lambda item, key = "name": item[key]

print(double(21)) # 42
print(sorted(["bb", "a", "ccc"], key = lambda s: -len(s))) # ["ccc", "bb", "a"]
print(pick({"name": "api"})) # api

Use def for anything that needs more than one expression, statements, or a name in tracebacks.

Operator precedence​

Operators are listed from the loosest binding to the tightest. Operators in the same row group from left to right, except where noted.

LevelOperators
1lambda
2x if cond else y
3or
4and
5not
6== != < > <= >= in not in (cannot be chained)
7|
8^
9&
10<< >>
11+ -
12* / // %
13unary + - ~
14x[i] x[a:b] x.name f(args)

Two consequences differ from some languages:

  • The bitwise operators bind more tightly than comparisons, so 5 & 3 == 1 is (5 & 3) == 1.
  • The not operator binds more loosely than comparison, so not 1 in [1] is not (1 in [1]).
print(1 + 2 * 3) # 7
print((1 + 2) * 3) # 9
print(-3 % 5) # 2
print(1 << 2 + 1) # 8
print(not 1 == 2) # True
print(1 or 0 and 2) # 1

When in doubt, add parentheses.

Next steps​