Python Assignment Statements
Python assignment statements are used to assign values to variables, forming the basis of data manipulation and program logic in Python.
Python assignment statements are statement forms that associate computed values with names, attributes, subscriptions, or recursively structured targets. These include regular assignment, augmented assignment, and annotated assignment, each with distinct evaluation and target-processing semantics.
Foundations of Python Assignment Statements
Assignment in Python applies a computed object to one or more targets. This operation associates the object with names or modifies attributes or items within existing objects. It is essential to distinguish between name binding and mutation of an existing object: name binding creates or changes the association of a name with an object, while mutation alters the contents or state of an object already referenced.
Only certain expressions can serve as assignment targets. Valid targets include names, attribute references, subscriptions (indexing), and recursively structured unpacking targets. Arbitrary expressions cannot be used as assignment targets because they do not represent locations where values can be stored.
Assignment does not inherently copy the assigned object. Multiple targets or names can reference the same mutable object, meaning changes through one reference affect all others referencing that object.
There are three principal assignment forms with different semantics:
- Regular assignment: Assigns a computed object to one or more targets, supporting unpacking and possibly multiple targets.
- Augmented assignment: Combines retrieval of a target's current value, a binary operation, and assignment back to the same target, supporting only single targets.
- Annotated assignment: Associates an annotation expression with one admissible target and optionally assigns a value, with special handling of annotation evaluation and target classification.
| Feature | Regular Assignment | Augmented Assignment | Annotated Assignment |
|---|---|---|---|
| Target Forms | Names, attributes, subscriptions, unpacking targets | Names, attributes, subscriptions only | Single admissible target only (no unpacking or chaining) |
| Number of Targets | One or more (including unpacking) | Exactly one | Exactly one |
| Right-hand-side Evaluation | Once per statement | Once per statement | Annotation expression evaluated as specified |
| Target Evaluation | Left-to-right, recursively for unpacking | Target evaluated once | Target components evaluated for non-simple targets |
| Unpacking Support | Yes | No | No |
| Possible In-Place Behavior | No (assigns result object) | Yes (may modify in-place) | No |
| Annotation Effects | None | None | Records annotation metadata, may defer evaluation |
Here is a concise executable example illustrating each assignment form:
# Regular assignment with unpacking
a, b = [1, 2] # assigns 1 to a, 2 to b
# Augmented assignment
a += 3 # increments a by 3 (in-place if possible)
# Annotated assignment
c: int = 5 # associates annotation 'int' with c, assigns 5
Effects:
- The regular assignment unpacks a list into two variables.
- The augmented assignment modifies
aby adding 3. - The annotated assignment records type metadata for
cand assigns it the value 5.
Regular Assignment Statements in Python
Regular assignment first evaluates the right-hand expression or expression list to produce a single resulting object. This object is then assigned recursively to each target list from left to right.
Name Assignment in Python
Assigning to an identifier binds or rebinds that name to the assigned object within the namespace determined by scope rules. Ordinary local binding affects the local namespace. The global statement directs assignment to the module-level namespace, while nonlocal directs assignment to an enclosing function scope, excluding the global scope.
Examples:
x = 10 # local binding of x
x = 20 # rebinding x locally
def f():
global y
y = 30 # binds y globally
def outer():
z = 40
def inner():
nonlocal z
z = 50 # modifies z in the outer function scope
inner()
return z
xis locally bound and rebound.yis bound globally insidef.zis modified in the enclosing scope byinner.
Rebinding a name changes which object the name references but does not mutate the original object. If other names reference the previous object, they remain bound to it unchanged.
Aliasing example:
lst1 = [1, 2]
lst2 = lst1 # lst2 references the same list object
lst1.append(3) # mutates the list; both see the change
lst2 = [4, 5] # rebinding lst2 to a new list; lst1 unchanged
Here, lst1 and lst2 initially alias the same mutable list. Mutating through lst1 is visible via lst2. Rebinding lst2 breaks the alias.
Attribute Assignment in Python
For an attribute assignment like obj.attr = value, Python first evaluates the primary expression obj to obtain the object. Then it requests that object to assign the supplied value to the named attribute attr.
This involves attribute-setting behavior that may invoke descriptors or custom hooks, which can validate, transform, redirect, or reject the assignment.
Reading obj.attr and assigning obj.attr = value need not operate on the same storage. For example, reading may find a class attribute, while assignment creates or updates an instance attribute, leaving the class attribute unchanged.
Example:
class C:
attr = 10
obj = C()
print(obj.attr) # 10 (from class attribute)
obj.attr = 20 # sets instance attribute 'attr'
print(obj.attr) # 20 (instance attribute shadows class attribute)
print(C.attr) # 10 (unchanged)
Descriptor-backed attributes like properties can make attribute assignment invoke behavior rather than simply place a value in an instance dictionary.
Example with property validation:
class D:
def __init__(self):
self._x = 0
@property
def x(self):
return self._x
@x.setter
def x(self, value):
if value < 0:
raise ValueError("x must be non-negative")
self._x = value
d = D()
d.x = 10 # valid assignment
# d.x = -5 # raises ValueError
Subscription Assignment in Python
Subscription assignment like obj[index] = value first evaluates the primary expression obj, then the subscription expression index. The resulting object receives the assigned value through its item-assignment behavior.
- Indexed assignment to mutable sequences interprets negative indexes relative to the sequence end and raises an error if the index is out of range.
- Mapping subscription assignment associates the key with the value, replacing or adding entries as appropriate.
- Slice assignment replaces a selected sequence region with values from an iterable, potentially changing the sequence length if permitted.
Examples:
lst = [1, 2, 3]
lst[1] = 20 # item assignment
d = {'a': 1}
d['b'] = 2 # dictionary key assignment
lst[1:3] = [30, 40] # slice replacement
lst[1:1] = [15, 25] # slice insertion (empty slice)
lst[1:3] = [] # slice deletion
Subscription assignment depends on the object's item-assignment protocol, so custom classes can define or restrict assignment semantics.
Example custom class:
class Custom:
def __setitem__(self, key, value):
print(f"Assigning {value} to key {key}")
c = Custom()
c['key'] = 'value' # prints: Assigning value to key key
Assignment Unpacking in Python
Assignment unpacking recursively assigns items from an iterable value into a target list with multiple targets.
- Fixed-length unpacking requires the source iterable to yield exactly as many items as non-starred targets.
Examples:
a, b = (1, 2) # succeeds
# a, b = (1,) # ValueError: too few values to unpack
# a, b = (1, 2, 3) # ValueError: too many values to unpack
- Extended unpacking uses one starred target to receive remaining items as a list.
Examples:
a, *b = [1, 2, 3] # a=1, b=[2,3]
*a, b = [1, 2, 3] # a=[1,2], b=3
a, *b, c = [1, 2, 3] # a=1, b=[2], c=3
a, *b = [1] # a=1, b=[]
- Nested unpacking recursively processes inner tuple-like or list-like targets.
Example:
(a, (b, c)) = (1, (2, 3)) # a=1, b=2, c=3
Swapping such as a, b = b, a works by fully evaluating the right-hand side before left-to-right target assignment, not by pairwise mutation.
Example:
a, b = 1, 2
a, b = b, a # swap values
Overlapping targets assign left to right:
lst = [0, 1]
a, lst[0] = 10, 20
# a is 10, lst becomes [20, 1]
Parentheses or brackets group targets structurally but do not construct tuple or list objects as assignment targets.
The right side of regular assignment can be:
- Single expression
- Starred expression (where valid)
- Expression list producing a tuple
- Yield expression (in a generator)
Chained assignments like a = b = expr evaluate the right-hand expression once and assign the same resulting object to each target from left to right.
Example:
lst1 = lst2 = []
lst1.append(1)
print(lst2) # [1]
This shows lst1 and lst2 alias the same list.
Assigning to a sequence of heterogeneous targets can cause observable interactions when an earlier assignment affects a value used later.
Example:
class C:
def __init__(self):
self.x = 0
c = C()
a = 1
a, c.x = 2, a
# a is 2, c.x is 1 (because right side fully evaluated before targets assigned)
| Target Form | Target Evaluation | Assignment Mechanism | Representative Failure Conditions |
|---|---|---|---|
| Name | Evaluate once, bind name | Bind/rebind name in appropriate namespace | SyntaxError if invalid name |
| Attribute | Evaluate primary once | Call attribute setter or assign in dict | AttributeError if attribute is read-only |
| Subscription | Evaluate container and index once | Call __setitem__ method | IndexError, KeyError, or custom error from protocol |
| Slice | Evaluate container and slice once | Call __setitem__ with slice | IndexError, TypeError |
| Fixed Unpacking | Evaluate target structure recursively | Assign each item to corresponding target | ValueError if length mismatch |
| Starred Unpacking | Evaluate target structure recursively | Assign remaining items to starred target | ValueError if multiple starred targets |
| Chained Assignment | Evaluate right side once | Assign same object to multiple targets | SyntaxError if targets invalid |
Augmented Assignment Statements in Python
Augmented assignment combines retrieval of one target value, a binary operation, and assignment of the operation result back to the original target.
Targets are restricted to a single name, attribute reference, or subscription; unpacking targets are not permitted.
The augmented target is evaluated only once, distinguishing target += value from a textual expansion such as target = target + value where the target expression might be evaluated multiple times.
Evaluation sequence:
- Evaluate and retrieve the left-hand target value.
- Evaluate the right-hand expression.
- Perform the corresponding operation (e.g., addition for
+=). - Assign the resulting object back to the original target.
Example demonstrating single evaluation of target and index expressions:
class SideEffectContainer:
def __getitem__(self, key):
print(f"Get item {key}")
return 10
def __setitem__(self, key, value):
print(f"Set item {key} = {value}")
container = SideEffectContainer()
index = 0
container[index] += 5
Output shows Get item 0 and Set item 0 = 15 printed once each, confirming single evaluation of target and index.
Although x += y and x = x + y may produce similar results, they are not semantically identical, especially when targets are complex expressions or objects support in-place operations.
Possible in-place behavior: mutable objects can preserve identity while changing value during augmented assignment. Immutable values require rebinding to a new object.
Examples contrasting rebinding and mutation:
x = 1
id_before = id(x)
x += 1
id_after = id(x)
print(id_before == id_after) # False (integers are immutable, new object)
lst = [1, 2]
id_before = id(lst)
lst += [3]
id_after = id(lst)
print(id_before == id_after) # True (list mutated in-place)
Aliasing consequences:
lst1 = [1]
lst2 = lst1
lst1 += [2] # lst2 sees mutation
lst1 = [1]
lst2 = lst1
lst1 = lst1 + [2] # lst1 rebound, lst2 unchanged
Augmented attribute assignment similarly evaluates the attribute target once, performs the operation, and assigns the result, subject to the same class-vs-instance and descriptor considerations as ordinary attribute assignment.
Augmented subscription assignment evaluates container and subscription expressions once, retrieves the current item, performs the operation with the right-hand value, and finally assigns the result back via item assignment.
Example custom class illustrating augmented assignment protocol:
class Number:
def __init__(self, value):
self.value = value
def __iadd__(self, other):
print("In-place add called")
self.value += other
return self
def __add__(self, other):
print("Ordinary add called")
return Number(self.value + other)
n = Number(10)
n += 5 # invokes __iadd__
If in-place operation is unavailable or declines (returns NotImplemented), Python falls back to the ordinary binary operation and assigns the result back to the target.
| Operator | Operation Family | Notes | |
|---|---|---|---|
| += | Addition | Supports in-place via __iadd__ | |
| -= | Subtraction | Supports in-place via __isub__ | |
| *= | Multiplication | Supports in-place via __imul__ | |
| @= | Matrix multiplication | Supports in-place via __imatmul__ | |
| /= | True division | Supports in-place via __itruediv__ | |
| //= | Floor division | Supports in-place via __ifloordiv__ | |
| %= | Modulo | Supports in-place via __imod__ | |
| **= | Exponentiation | Supports in-place via __ipow__ | |
| <<= | Left shift | Supports in-place via __ilshift__ | |
| >>= | Right shift | Supports in-place via __irshift__ | |
| &= | Bitwise AND | Supports in-place via __iand__ | |
| ^= | Bitwise XOR | Supports in-place via __ixor__ | |
| = | Bitwise OR | Supports in-place via __ior__ |
Failures in augmented assignment can occur at any stage: target retrieval, right-hand evaluation, operation, or assignment. Observable side effects prior to failure remain.
Example tracing augmented assignment:
class Container:
def __getitem__(self, key):
print(f"Retrieving item {key}")
return 10
def __setitem__(self, key, value):
print(f"Assigning item {key} = {value}")
c = Container()
i = 0
c[i] += 5
Annotated Assignment Statements in Python
Annotated assignment associates an annotation expression with one admissible target and optionally performs an ordinary value assignment to that same target.
Only one target is accepted, with no unpacking or chained targets allowed.
The source form:
target : annotation [= value]
with an annotation expression following a colon, optionally followed by an equals sign and an assignment expression.
A simple annotated target is an unparenthesized name. Non-simple targets include attributes, subscriptions, or parenthesized names.
In Python 3.14 and later, annotations for simple annotated targets at module or class scope are collected lazily rather than evaluated immediately during execution. Retrieval of annotations uses supported facilities like annotationlib.get_annotations or the object's annotation metadata, which may trigger deferred evaluation.
For non-simple annotated targets, the annotation expression is never evaluated.
Example (Python 3.14+):
x: int = 5 # simple target, annotation evaluated lazily
(y): int = 6 # parenthesized name, non-simple target, annotation not evaluated
obj.attr: int # attribute target, annotation not evaluated
Function-scope annotated names serve for scope classification. Their annotation expressions are not evaluated or stored as function variable annotations.
Example:
def func():
value: int # declares 'value' local, no runtime binding
value = 10 # explicit binding
When an annotated assignment includes a right-hand side, the value assignment proceeds as in regular assignment.
When no right-hand side is present, the statement records annotation metadata or local-name classification but does not assign a runtime value.
Contrasting annotations:
x: int # annotation only, no value assigned
y: int = 10 # annotation with value assignment
For attribute or subscription targets without a right-hand value, Python evaluates target components to identify the target expression but does not perform the final attribute or item assignment or evaluate the non-simple annotation.
Example (Python 3.14+):
class C:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
@value.setter
def value(self, v):
print(f"Setting value to {v}")
self._value = v
c = C()
c.value: int # evaluates c and 'value' but does not assign or evaluate annotation
c.value: int = 10 # performs assignment and annotation
Parenthesizing an otherwise simple annotated name changes it to non-simple for annotation handling, even though it remains a valid assignment target.
Annotations serve as metadata only; they do not enforce runtime type checking, prevent rebinding, validate values, or restrict assignment.
| Target Classification | Annotation Evaluation | Annotation Storage or Retrieval | Local-Name Effect | Value Assignment Allowed |
|---|---|---|---|---|
| Simple module target | Evaluated lazily at module execution | Stored in module annotations | Declares module-level annotated name | Optional |
| Simple class target | Evaluated lazily at class execution | Stored in class annotations | Declares class attribute annotation | Optional |
| Function-local name | Not evaluated | Not stored as function variable annotation | Declares local variable annotation | Optional |
| Parenthesized name | Not evaluated | No annotation recorded | Declares local variable annotation | Optional |
| Attribute target | Not evaluated | No annotation recorded | No local binding effect | Optional |
| Subscription target | Not evaluated | No annotation recorded | No local binding effect | Optional |
Example illustrating lazy annotations and classification:
class Cls:
a: int # lazy evaluation, stored in Cls.__annotations__
(b): int # non-simple, annotation not evaluated
c = Cls()
print(Cls.__annotations__) # {'a': int}
The Python 3.14 changes to lazy evaluation and non-evaluation of non-simple annotations are version-dependent and do not apply to all earlier Python 3 releases.
Annotated assignments differ from type comments, function parameter annotations, return annotations, type alias statements, and static type-checker interpretation, which are outside the scope of this treatment.
Solved Python Assignment Exercises
class Example:
def __init__(self):
self.value = 0
@property
def value_prop(self):
return self.value
@value_prop.setter
def value_prop(self, v):
print(f"Setting value_prop to {v}")
self.value = v
def __getitem__(self, key):
print(f"Getting item {key}")
return self.value + key
def __setitem__(self, key, val):
print(f"Setting item {key} = {val}")
self.value = val - key
ex = Example()
# Regular name assignment
a = 1
# Attribute assignment
ex.value = 2
# Subscription assignment
ex[3] = 5
# Slice assignment on list
lst = [1, 2, 3, 4]
lst[1:3] = [9, 8]
# Chained assignment
x = y = [0]
# Nested unpacking
(p, (q, r)) = (10, (20, 30))
# Starred unpacking
s, *t, u = [1, 2, 3, 4, 5]
# Augmented assignment with observable evaluation and possible in-place behavior
lst += [7]
# Annotated assignments (Python 3.14+)
simple_ann: int = 42
non_simple_ann: (int) = 43
attr_ann: int = 0
ex.attr: int = 100 # annotation-only attribute assignment (no value assigned)
ex[0]: int # annotation-only subscription assignment
print(f"a={a}, ex.value={ex.value}, lst={lst}, x={x}, y={y}")
print(f"p={p}, q={q}, r={r}, s={s}, t={t}, u={u}")
print(f"simple_ann={simple_ann}, non_simple_ann={non_simple_ann}, ex.attr={ex.attr}")
print(f"x is y: {x is y}")
Explanation
- Right-hand sides evaluate once: tuple
(10, (20, 30))for nested unpacking, list[1,2,3,4,5]for starred unpacking. - Targets assigned left to right, recursively for unpacking.
- Overlapping targets
xandyassigned same list object (aliasing). - Augmented assignment
lst += [7]mutates list in place, preserving identity. - Simple annotated target
simple_annevaluates annotation lazily and assigns value. - Non-simple annotated target
(int)assigned tonon_simple_anndoes not evaluate annotation. - Attribute annotation
ex.attr: int = 100assigns value and records annotation metadata. - Attribute annotation-only
ex.attr: intevaluates target but does not assign or evaluate annotation. - Subscription annotation-only
ex[0]: intevaluates target components but does not assign or evaluate annotation.
Assignment reasoning involves four key questions:
-
What value is computed?
The right-hand side expression is evaluated once to produce the value. -
What target form is being assigned?
Targets may be names, attributes, subscriptions, or unpacked recursively. -
When and how often are target components evaluated?
Targets are evaluated left to right, with single evaluation for augmented assignments. -
What is the operation?
It may bind a name, delegate attribute or item mutation, perform an in-place-capable operation, or only record annotation metadata without value assignment.
This structured approach clarifies Python assignment semantics across regular, augmented, and annotated forms.