Numbers as Objects
Every Python number is an object with a numeric type. That type determines which attributes and methods you can access through dot notation.
Writing creates an int, while writing creates a float. The built-in numeric objects covered here are immutable. A method call such as conjugate() returns a result without changing the original object's value.
If a = 5, the assignment a = a + 1 binds the name a to an object whose value is 6. Distinguish object identity from name assignment: the object whose value was 5 has not changed into 6.
Accessing Attributes and Methods
Attribute access reads a value through a named object member, while a method call performs an operation. In general, an attribute may also compute a value when accessed. You read an attribute with object.attribute and call a method with object.method(). The parentheses in the second form are what tell Python to run the method.
A complex number shows the difference clearly. The expression c.real is data that Python already stores inside c, and attribute access returns that real component. The call c.conjugate() is work Python performs at that moment, and the result is a second complex number that leaves c untouched.
# Creating a complex number
c = 2 + 3j
# Accessing real attribute (real part)
print(c.real) # Output: 2.0
# Accessing imag attribute (imaginary part)
print(c.imag) # Output: 3.0
# Calling conjugate() method to get conjugate
print(c.conjugate()) # Output: (2-3j)Identifying the Numeric Type
Python chooses the numeric type from the literal you write, and that type decides which attributes and methods the value carries. Asking Python for the class before you call a type-specific method keeps you from guessing whether that operation exists on that value.
The type() function returns the class of the value, which is a class object, not a string naming the type. The three literals below look similar when you pass them to a function, but type() reports a different class for each one.
Using the Type Function
Distinguish literals from expressions: 3j is an imaginary literal, while 2 + 3j is an addition that produces a complex object.
# Viewing number data types
print(type(42)) # Output: <class 'int'>
print(type(3.14)) # Output: <class 'float'>
print(type(2+3j)) # Output: <class 'complex'>| Expression | Class that type() reports | Reason Python chooses that class |
|---|---|---|
42 | int | The literal has no decimal point and no exponent |
3.14 | float | The literal contains a decimal point |
2+3j | complex | Adding an integer and the imaginary literal 3j produces complex |
Reading the Parts of a Complex Number
A complex number keeps two components apart, and Python exposes both as attributes instead of making you unpack them. The real attribute holds the real component, the imag attribute holds the coefficient of the imaginary unit, and both come back as floats even when you wrote whole numbers.
Each attribute returns one component of the number. Comparing a pure real number, a pure imaginary number, and a mixed number shows which component belongs to which attribute.
The real Attribute
The real attribute returns the real component of the number. Reading it never changes the number, so you can inspect the same value as often as you need.
# Example usage of real attribute
z1 = 4 + 5j
z2 = -2 + 7j
z3 = 10 + 0j # Pure real number
print(f"Real part of {z1} is {z1.real}")
print(f"Real part of {z2} is {z2.real}")
print(f"Real part of {z3} is {z3.real}")
# Output:
# Real part of (4+5j) is 4.0
# Real part of (-2+7j) is -2.0
# Real part of (10+0j) is 10.0The imag Attribute
The imag attribute returns the coefficient of . Python writes that unit as j inside a complex literal. The sign belongs to the coefficient, so a subtracted term comes back as a negative number.
# Example usage of imag attribute
z1 = 3 + 8j
z2 = 6 - 4j
z3 = 0 + 9j # Pure imaginary number
print(f"Imaginary part of {z1} is {z1.imag}")
print(f"Imaginary part of {z2} is {z2.imag}")
print(f"Imaginary part of {z3} is {z3.imag}")
# Output:
# Imaginary part of (3+8j) is 8.0
# Imaginary part of (6-4j) is -4.0
# Imaginary part of 9j is 9.0The result for z2 is , because the coefficient carries the minus sign that stands in front of the imaginary unit. The value for z1 stays positive, and the pure imaginary number z3 reports with a real component of .
Methods on Complex Numbers
Complex numbers also carry their own methods, and the ones below cover the operations you reach for most often. The conjugate() method returns a complex number, so you can keep calling methods on the result without changing the value you started from.
The Conjugate Method
For , conjugate() returns . The real part keeps its value and only the imaginary part changes sign, which makes the conjugate the mirror image of the number across the real axis.
# Example usage of conjugate() method
z1 = 3 + 4j
z2 = -2 - 5j
z3 = 7 + 0j
print(f"Conjugate of {z1} is {z1.conjugate()}")
print(f"Conjugate of {z2} is {z2.conjugate()}")
print(f"Conjugate of {z3} is {z3.conjugate()}")
# Output:
# Conjugate of (3+4j) is (3-4j)
# Conjugate of (-2-5j) is (-2+5j)
# Conjugate of (7+0j) is (7-0j)The second result shows the rule on a negative coefficient. The imaginary part of z2 is , so its conjugate carries instead, while the real part stays at . A real number such as z3 is its own conjugate, because its imaginary part is already zero.
Computing the Size of a Complex Number
The identity connects a complex number to its modulus, and the example below works that identity out in code. In ordinary Python code you would call abs(z) for the modulus, so read this example as the reason that built-in works.
import math
# Calculating complex number modulus using conjugate
z = 3 + 4j
modulus_squared = z * z.conjugate()
modulus = math.sqrt(modulus_squared.real)
print(f"Complex number: {z}")
print(f"Its conjugate: {z.conjugate()}")
print(f"z × z*: {modulus_squared}")
print(f"Modulus |z|: {modulus}")
print(f"Built-in abs(z): {abs(z)}")
# Output:
# Complex number: (3+4j)
# Its conjugate: (3-4j)
# z × z*: (25+0j)
# Modulus |z|: 5.0
# Built-in abs(z): 5.0Multiplying by its conjugate cancels the imaginary part and leaves the real number . Taking the square root of that real number returns , and the built-in abs(z) reports the same value in one call.
Methods on Integers and Floats
Integers and floats expose methods that match their own representation, and those methods are not interchangeable with the complex ones. bit_length() counts bits in the absolute integer value, to_bytes() writes those bits into a byte sequence, is_integer() checks whether a float has a fractional part, and as_integer_ratio() reports the fraction a float actually stores.
| Method | Type it belongs to | Question it answers |
|---|---|---|
bit_length() | int | How many bits does the absolute value need |
to_bytes() | int | Which bytes represent this value in a chosen width and order |
is_integer() | float | Does this float have no fractional part |
as_integer_ratio() | float | Which exact fraction does the stored value equal |
Modern Python also gives int the methods is_integer() and as_integer_ratio(): (42).is_integer() returns True, and (42).as_integer_ratio() returns (42, 1). The table selects the types used in these examples rather than listing every type that owns a method.
Integer Numbers
# Some methods on integer numbers
n = 42
# bit_length() method - calculates the number of bits needed
print(f"Number of bits for {n}: {n.bit_length()}")
# to_bytes() method - converts to bytes
byte_representation = n.to_bytes(2, byteorder='big')
print(f"Byte representation of {n}: {byte_representation}")
# Output:
# Number of bits for 42: 6
# Byte representation of 42: b'\x00*'bit_length() counts the bits needed for the absolute integer value, excluding its sign and leading zeros. Here, to_bytes() receives an explicit output length and byte order, so the two-byte result above begins with a zero byte.
Float Numbers
# Some methods on float numbers
f = 3.14159
# is_integer() method - checks if float is a whole number
print(f"{f} is a whole number: {f.is_integer()}")
print(f"{4.0} is a whole number: {(4.0).is_integer()}")
# as_integer_ratio() method - returns ratio as a fraction
ratio = f.as_integer_ratio()
print(f"Ratio of {f}: {ratio}")
# Output:
# 3.14159 is a whole number: False
# 4.0 is a whole number: True
# Ratio of 3.14159: (3537115888337719, 1125899906842624)is_integer() checks whether a finite float has no fractional part, so fails that check while passes it. as_integer_ratio() returns the exact fraction stored in binary, which is why the ratio for holds such large numbers instead of the decimal fraction a reader would expect.
Validating and Calculating with Numeric Objects
Attributes and methods also support checks and conversions, and those two steps belong together. You check the input first and you calculate second, so values that violate the chosen domain rules can be rejected before arithmetic.
Checking the Input Before Calculating
Attributes can carry explicit validation rules. The function below requires a complex object first, then applies two example domain rules to its components and reports a message for each case it rejects.
def validate_complex_input(z):
"""Function to validate complex number input"""
if not isinstance(z, complex):
return False, "Input is not a complex number"
if z.real < 0:
return False, "Real part cannot be negative"
if z.imag == 0:
return False, "Imaginary part cannot be zero"
return True, "Valid input"
# Testing validation function
test_numbers = [3+4j, -2+5j, 7+0j, 2.5+1.5j]
for num in test_numbers:
is_valid, message = validate_complex_input(num)
print(f"{num}: {message}")The function returns a pair of values, so the caller receives the decision and the reason together. Of the four test numbers, and pass all three checks, is rejected for a negative real part, and is rejected because its imaginary part is zero.
Calculating with Complex Numbers
def complex_operations(z1, z2):
"""Performing various operations on complex numbers"""
print(f"Number 1: {z1}")
print(f"Number 2: {z2}")
print(f"Real part of z1: {z1.real}")
print(f"Imaginary part of z1: {z1.imag}")
print(f"Conjugate of z1: {z1.conjugate()}")
# Mathematical operations
addition = z1 + z2
multiplication = z1 * z2.conjugate()
print(f"z1 + z2 = {addition}")
print(f"z1 × z2* = {multiplication}")
# Usage example
z1 = 2 + 3j
z2 = 1 - 2j
complex_operations(z1, z2)With those inputs, z1 + z2 is , because the real parts and the imaginary parts add separately. The expression z1 * z2.conjugate() multiplies by and returns .
Identify the numeric type, then check whether an operation returns a component, a representation, or a new value, because that return type decides how the next expression can use the result.