Skip to main content

Data Types

Every value in the Atmos Automation Language has a type. This page describes each built-in type, how to create values of that type, which operations they support, and how mutability and freezing affect what a program can change.

The built-in type() function returns the name of a value's type as a string:

print(type(None)) # NoneType
print(type(True)) # bool
print(type(1)) # int
print(type(1.5)) # float
print(type("a")) # string
print(type(b"a")) # bytes
print(type([])) # list
print(type(())) # tuple
print(type({})) # dict
print(type(set())) # set
print(type(range(3))) # range
print(type(len)) # builtin_function_or_method
print(type(lambda: 1)) # function

Summary​

TypeExampleMutableHashable
NoneTypeNoneNoYes
boolTrue, FalseNoYes
int42, 0xFFNoYes
float3.14, 1e3NoYes
string"text"NoYes
bytesb"\x00\x01"NoYes
list[1, 2, 3]YesNo
tuple(1, "a")NoOnly if every element is hashable
dict{"k": "v"}YesNo
setset([1, 2])YesNo
functiondef f(): ...NoYes

A hashable value can be a dictionary key or a set element. Using an unhashable value as a key raises an error such as unhashable type: list.

None​

The type NoneType has one value, None. It represents "no value" and is what a function returns when it has no return statement. Compare against it with ==; the is operator is reserved.

def find(items, wanted):
for item in items:
if item == wanted:
return item

print(find([1, 2], 3)) # None
print(find([1, 2], 3) == None) # True

Booleans​

The two boolean values are True and False. Comparisons produce booleans, and bool(x) converts any value using the truthiness rules below.

Booleans and integers are separate types: adding True + 1 is an error, and the comparison True == 1 is False. Convert explicitly with int(True).

Integers​

Integers have arbitrary precision. They never overflow, so results are exact however large they grow:

def power_of_two(n):
result = 1
for _ in range(n):
result = result * 2
return result

print(power_of_two(100)) # 1267650600228229401496703205376

Integer division behaves as follows:

  • Division with / always returns a float: 7 / 2 is 3.5 and 4 / 2 is 2.0.
  • Floor division with // rounds toward negative infinity: -7 // 2 is -4.
  • The remainder operator % takes the sign of the right operand, so -7 % 3 is 2 and 7 % -3 is -2.
  • Dividing by zero is an error.

Bitwise operators (&, |, ^, ~, <<, >>) work on integers only. A left shift count must be below 512. Convert other values with int(): it accepts floats (truncating toward zero), booleans, and strings, with an optional base.

print(int("42")) # 42
print(int("ff", 16)) # 255
print(int(-3.9)) # -3
print(int(True)) # 1

Floats​

Floats are IEEE 754 double-precision numbers. A float and an integer compare by exact numeric value, so 1 == 1.0 is True. Arithmetic that mixes the two produces a float.

Float division by zero is an error rather than infinity, but overflow produces infinity, and float("inf"), float("-inf"), and float("nan") create the special values.

print(1 / 3) # 0.3333333333333333
print(7.5 // 2) # 3.0
print(-7.5 % 2) # 0.5
print(float("1e3")) # 1000.0
print(1e308 * 10) # +inf

See Lexical Elements for how floats are printed.

Strings​

A string is an immutable sequence of text, stored as UTF-8. Create strings with string literals, with str(value), or with methods such as format() and join().

Strings support these operations:

  • Concatenation with + and repetition with *: "ab" * 2 is "abab".
  • Substring tests with in: "an" in "banana" is True.
  • Comparison with <, ==, and the other comparison operators. Ordering is lexicographic, so "Z" < "a" is True.
  • Indexing and slicing, described below.
  • Formatting with % and .format(). See Expressions and Operators.
  • Methods such as split, strip, replace, startswith, lower, and upper. See Methods.

Indexing and slicing operate on bytes, and len() returns the number of bytes. For ASCII text each character is one byte, but a character such as é takes two:

s = "héllo"
print(len(s)) # 6
print(s[0]) # h
print(s[-1]) # o
print(s[0:3]) # hé
print(list(s.codepoints())) # ["h", "é", "l", "l", "o"]
print(len(list(s.codepoints()))) # 5

A string is not directly iterable, and list("abc") is an error. Choose how to iterate: .elems() yields each byte as a one-byte string, and .codepoints() yields each Unicode character. The .elem_ords() and .codepoint_ords() variants yield integers.

for ch in "ab".codepoints():
print(ch)
# a
# b

Because strings are immutable, s[0] = "x" is an error. Build a new string with slicing, replace, or concatenation instead.

Bytes​

A bytes value is an immutable sequence of byte values, created with the b"..." literal or with bytes(value). It supports +, *, in, len(), comparison, and slicing. Indexing returns a one-byte bytes value. The .elems() method yields each byte as an integer. A bytes value is not directly iterable.

data = b"abc"
print(len(data)) # 3
print(data[1:]) # bc
print(97 in data) # True
print(list(data.elems())) # [97, 98, 99]

Lists​

A list is an ordered, mutable sequence. Write one with square brackets, or build one with a comprehension.

def demo():
items = ["api", "worker"]
items.append("cron")
items[0] = "web"
items.extend(["queue"])
return items

print(demo()) # ["web", "worker", "cron", "queue"]

List operations:

  • Indexing with items[i], where negative indexes count from the end, and slicing with items[start:stop:step].
  • Assignment to an existing index, items[i] = value. Assigning to a slice or an index that is out of range is an error.
  • Concatenation with + and repetition with *: [0] * 3 is [0, 0, 0].
  • Methods: append, extend, insert, pop, remove, index, and clear.
  • Ordered comparison, element by element: [1, 2] < [1, 3] is True.

Adding to a list or changing an element while iterating over that same list is an error (cannot append to list during iteration). Iterate over a copy such as items[:] when the loop needs to change the list.

Tuples​

A tuple is an immutable ordered sequence. Write tuples with parentheses; a one-element tuple needs a trailing comma.

point = (3, 4)
single = (1,)
empty = ()
x, y = point
print(x + y) # 7
print(point + (5,)) # (3, 4, 5)
print(point == (3, 4)) # True

Tuples support indexing, slicing, +, *, in, len(), and comparison. A tuple of hashable values is itself hashable, which makes tuples useful as dictionary keys. Converting between sequences uses list(t) and tuple(l). Unparenthesized tuples are legal on the right side of an assignment and in return, but a bare trailing comma such as x = 1, is a syntax error.

Dictionaries​

A dictionary maps keys to values and keeps keys in insertion order. Write one with braces, or with the dict() function.

def demo():
env_vars = {"REGION": "us-east-1", "STAGE": "dev"}
env_vars["TIER"] = "web"
env_vars["STAGE"] = "prod"
return env_vars, list(env_vars), len(env_vars)

print(demo())
# ({"REGION": "us-east-1", "STAGE": "prod", "TIER": "web"}, ["REGION", "STAGE", "TIER"], 3)

Dictionary behavior:

  • Reading a missing key with d[key] is an error. Use d.get(key), which returns None, or d.get(key, default).
  • Iterating a dictionary, and the in operator, work on keys. Use .items(), .keys(), and .values() for other views; each returns a list.
  • Overwriting an existing key keeps its original position. A new key goes at the end.
  • A key must be hashable. The integer 1 and the float 1.0 are the same key.
  • Repeating a key in a dictionary literal is an error (duplicate key).
  • Two dictionaries are equal when they hold the same keys and values, whatever the order.
  • The expression d | other returns a new dictionary with the entries of other taking precedence, and d |= other updates in place.
  • Assigning, inserting, or deleting keys while iterating the same dictionary is an error, even when the key already exists. Loop over list(d) to change a dictionary inside the loop.

Other methods are pop, popitem, setdefault, update, and clear. See Methods.

Sets​

A set is a collection of unique hashable values. Iterating or printing a set follows insertion order. Create one with set(iterable); there is no set literal.

def demo():
a = set([1, 2, 2, 3])
b = set([3, 4])
return len(a), a | b, a & b, a - b, a ^ b

print(demo())
# (3, set([1, 2, 3, 4]), set([3]), set([1, 2]), set([1, 2, 4]))

Sets support in, len(), iteration, and these operators: | (union), & (intersection), - (difference), and ^ (symmetric difference). Methods include add, remove, discard, pop, update, union, intersection, difference, symmetric_difference, issubset, issuperset, and clear. Adding to a set while iterating it is an error.

Elements must be hashable, so a set of lists is an error but a set of tuples works. The empty set is set(); {} is an empty dictionary.

Ranges​

Calling range(stop), range(start, stop), or range(start, stop, step) returns an immutable sequence of integers computed on demand. It supports indexing, len(), in, and iteration.

print(list(range(3))) # [0, 1, 2]
print(list(range(1, 10, 3))) # [1, 4, 7]
print(list(range(5, 0, -2))) # [5, 3, 1]

Functions​

Functions defined with def or lambda are values: they can be assigned to names, stored in lists and dictionaries, passed as arguments, and returned from other functions. A function defined inside another function keeps access to the outer function's variables (a closure).

def make_adder(n):
def add(x):
return x + n
return add

add5 = make_adder(5)
handlers = {"add5": add5, "double": lambda x: x * 2}
print(add5(1)) # 6
print(handlers["double"](4)) # 8

Built-in functions such as len and methods such as "".upper have the type builtin_function_or_method. See Statements for defining functions and Built-in Functions for the functions that ship with the language.

Modules and structs​

Atmos supplies several predeclared values that come from the runtime rather than from literals.

  • Modules such as exec, fs, steps, components, regex, json, ui, log, atmos, cli, and dependencies have type module. Read members with dot syntax: exec.run, json.encode. A module cannot be modified, and reading a name it does not define is an error. See the automation functions reference.
  • Structs have type struct. Fields are read with dot syntax. A struct's fields cannot be assigned or deleted, and dir(value) lists them. The result of exec.run is a struct with stdout, stderr, and exit_code fields. ctx is a struct whose fields describe the invocation.
  • The env value is a read-only dictionary of the explicit step inputs. See env.
result = exec.run(["echo", "hi"], output="capture")
print(type(result)) # struct
print(result.exit_code) # 0
print(result.stdout.strip()) # hi
print(type(exec)) # module
print(type(env)) # dict

Truthiness​

Conditions in if, while, and, or, not, comprehensions, and conditional expressions accept any value. The following values are false:

  • None
  • False
  • the integer 0 and the floats 0.0 and -0.0
  • the empty string "" and empty bytes b""
  • empty collections: [], (), {}, and set()
  • an empty range such as range(0)

All other values are true, including the string "0", the string "False", the list [0], and float("nan"). Use bool(x) to convert explicitly.

for value in [None, 0, "", [], {}, "0", [0], 1.5]:
print(repr(value), bool(value))
# None False
# 0 False
# "" False
# [] False
# {} False
# "0" True
# [0] True
# 1.5 True

Equality and ordering​

Equality with == compares by value: lists, tuples, dictionaries, and sets are equal when their contents are equal. A list never equals a tuple, even with the same elements. Integers and floats compare by numeric value.

The ordering operators <, <=, >, and >= work between numbers, between strings, between bytes, and between lists or tuples, which compare element by element. Ordering mixed types, such as 1 < "a", or ordering dictionaries, is an error. The sorted() function raises the same error for a list that mixes strings and numbers.

Mutability and freezing​

Strings, numbers, booleans, None, tuples, and bytes cannot change. Lists, dictionaries, and sets can change in place. Variables hold references, so two names can refer to one list:

def demo():
a = [1, 2]
b = a
b.append(3)
return a # a sees the change

print(demo()) # [1, 2, 3]

Pass a copy when a function should not affect the caller's list: list(a) or a[:] for lists, dict(d) for dictionaries.

A frozen value rejects every change, including changes to values nested inside it. Attempting one fails with an error such as cannot append to frozen list or cannot insert into frozen hash table. Values become frozen in these situations:

  • After a file finishes loading. Every top-level value in a file brought in with load() becomes frozen. The importing file can read these values, but cannot modify them.
  • When parallel work starts. Values that tasks started by steps.parallel can reach become frozen. Prepare shared lists and dictionaries before dispatching tasks.
  • For values Atmos supplies. env, ctx.flags, and ctx.arguments are frozen dictionaries.
  • Temporarily during iteration. A collection being looped over rejects structural changes until the loop ends.
shared = {"regions": ["us-east-1"]}
shared["regions"].append("eu-west-1") # allowed: not frozen yet

def count():
return len(shared["regions"])

output = steps.parallel(functions = [count])
shared["extra"] = 1 # error: cannot insert into frozen hash table

Copy a frozen collection into a new one to get a modifiable version. For example, dict(env) returns a normal dictionary you can change.

Next steps​