Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
324: Working with bytes and bytearray
Up until now, we've spent most of our time dealing with strings. But in the real world, software engineers often have to step away from human-readable text and deal with raw binary data—whether that's reading a JPEG, communicating with a piece of hardware via a socket, or parsing a custom file format. That's where bytes and bytearray come in.
To show you how this works, I'm going to build a simple "packet builder." Imagine we're writing a driver for a temperature sensor. The sensor expects a 4-byte packet: a start byte (0x02), a command byte, a data value, and a checksum.
Defining our packet structure
First, I'll start by creating the packet using the bytes type. I like using hex notation (0x..) here because it's the industry standard for binary data; seeing 165 is far less intuitive than seeing 0xA5.
# Let's define a basic "Read Temperature" packet
# Start byte: 0x02, Command: 0x10, Value: 0x00, Checksum: 0x00
packet = bytes([0x02, 0x10, 0x00, 0x00])
print(f"Initial packet: {packet}")
# Output: b'\x02\x10\x00\x00'
Notice the b prefix in the output. That's Python telling you this isn't a string; it's a bytes object. Each element is an integer from 0 to 255.
The immutable wall
Now, here is where I usually trip up when I'm rushing a project. I need to update the third byte to reflect the actual temperature value (let's say 25 degrees Celsius, which is 0x19 in hex) and then calculate a simple checksum.
# I want to update the value to 25 (0x19)
packet[2] = 0x19
If you run that, Python will throw a TypeError: 'bytes' object does not support item assignment. I forgot that bytes objects are immutable, just like strings. Once I've created that packet, I can't change a single bit of it. If I want to modify the data, I have two choices: create a brand new bytes object through concatenation, or use a bytearray.
Switching to bytearray for flexibility
Since we're building a packet that needs several updates before it's "ready," a bytearray is the right tool for the job. It's essentially a mutable version of bytes. Let's rewrite this logic.
# Convert our template to a bytearray
packet = bytearray([0x02, 0x10, 0x00, 0x00])
# Now we can actually change the values
packet[2] = 0x19 # Temperature value
# Let's calculate a primitive checksum: sum of first 3 bytes modulo 256
packet[3] = sum(packet[:3]) % 256
print(f"Updated packet: {packet}")
# Output: bytearray(b'\x02\x10\x19\x2b')
This is much cleaner. I can treat the bytearray like a list of integers, which makes the logic for calculating checksums or manipulating headers a lot easier to write and read.
Locking it down for transmission
Once the packet is constructed, you generally don't want it to be mutable anymore. If you're passing this packet to a network library or writing it to a file, those functions usually expect a bytes object. It's a safety measure—you don't want some other part of your program accidentally changing the checksum while the packet is in the send queue.
# Cast it back to immutable bytes for sending
final_packet = bytes(packet)
print(f"Ready to send: {final_packet}")
# Output: b'\x02\x10\x19\x2b'
And there you have it. Use bytearray when you're "assembling" or "editing" binary data, and cast it to bytes when it's time to ship it.
📋 Practical Task
Exercise: Building a XOR Binary Obfuscator
In some low-level protocols, data is "obfuscated" using a simple XOR operation to prevent plain-text reading. Your task is to write a function called obfuscate_data that takes a bytes object and a single-byte integer key.
- The function should convert the
bytesinput into abytearray. - Loop through every byte in the array and perform a XOR operation (using the
^operator) with the provided key. - Return the final result as an immutable
bytesobject.
Test your code with this:
key = 0x42
original = b"Secret Message"
result = obfuscate_data(original, key)
print(result) # Should be a bytes object with altered values
print(obfuscate_data(result, key)) # XORing again with the same key should return the original message!
There are no comments for now.