Beginner
The need to print when you program is of course one of the most important, and probably the very first things you ever did! This is your full guide on how to print for both python 2 and python 3).
The quickest and simplest scenario on how to print is to simply write the following:
print("Hello World")

However, there are many other variations of printing that comes up when you are coding in python. These could be printing json files, printing without a new line, printing to a log file, printing formatted text, and many more. Find below what you’re looking for in this one stop guide to printing!
Printing without a new line
When you normally use the print(“abc”) construct it still adds a new line character. In order to print without a new line use the end parameter.
print("Hello World", end="")
Normally when printing:

See example with the print parameter:

Printing Text together
When printing items, there are often times you need to print text write next to each other, or you need concatenate text together. Concatenating text in Python is simple and can be done in several ways.
Note that in the below approach that for the method 2, there is no space between the text which is why method 3 helps to solve this problem.
text1 = 'shoe'
text2 = 'laces'
print("Method 1:", text1, text2)
print("Method 2:", text1 + text2)
print("Method 3:", text1 + ' ' + text2)
print("Method 4:", "%s %s" % ( text1, text2) )

Formatting numeric output when printing
When printing, often it’s needed to format the print output. Here’s a list of formatting scenarios.
Printing a number with a string
When printing a number, it is typically simple to do with the following statement:
counter = 5
print(counter)

The problem arises when you want to print text along the same line. You will typically get this error TypeError: unsupported operand type(s) for +: ‘int’ and ‘str’ . For example for the following:

The trick is that you can always concatenate two strings together. Hence, you simply need to convert the int (short for integer, or whole number) into a str (string).
counter = 5
print( str(counter) + ' apples' )

Padding zeros when printing numbers
When printing numbers, often you need to pad with zeros. There are multiple ways to do this, but one of the easier ways is to first convert the number to a string, and then use the zfill function of the string where you can specify how long the number should be.
#this prints the number 10 with up to 8 padded zeros
counter = 10
str(counter).zfill(8)
A more advanced example follows where we’re printing 7 numbers. Notice that for the last number where the number is more than 8 digits, that there are no padded zeros.
counter = 2
for x in range(1, 7):
print( str(counter).zfill(8) )
counter = counter * counter

Another method to pad zeros is the following method to use the format function where a zero is placed in front of the number of digits. Here the “08” refers to padding zeros for 8 digits
counter = 2
for x in range(1, 7):
print( format(counter, '08'))
counter = counter * counter

Printing with text alignment
The following can be used when you want to print a table of contents where the structure “{:>nn”.format(‘text to format’) is used. nn is the number of letters to pad.
'{:>15}'.format('text')
Without any alignment:

With alignment:

Printing complex data structures in readable format
One of the great things about python is that you can put together complex data structures fairly easily. This could be a dictionary where each dictionary item is a list. However, to print this out normally is quite difficult to read. This is where pretty print comes in. Suppose you have the following structure:

Within python, this is represented as a dictionary where the main items “furniture” and “appliances” have the sub-items. So if the data structure is “listitems”, then the data coudl be represented as follows:
listitems ={ 'furniture':[ 'desk', 'chair', 'sofa'], 'appliances':['tv', 'lamp', 'hifi']}
With this in mind, then printing of this data would be as follows:
listitems ={ 'furniture':[ 'desk', 'chair', 'sofa'], 'appliances':['tv', 'lamp', 'hifi']}
print(listitems)

This is where the import library pprint comes in. You can simply use this to print out the output in a more readable fashion. There are two important parameters though. You should use the indent parameter to specify how much space there is per element, and then width to ensure that limited items are put on a single two. If you put a width of 1 character, then that’ll ensure only show one element at most (so if a element has more than 1 character it’s ok, but you cannot include a second element in there as you’re already the 1 width limit).
import pprint;
listitems ={ 'furniture':[ 'desk', 'chair', 'sofa'], 'appliances':['tv', 'lamp', 'hifi']}
pprint.pprint(listitems, indent=1, width=1)

Printing time
Printing time is another important item that you tend to do often in case you want to monitor performance or perhaps to give an update that your long operation is still running.
Print the time
First lets simply print the current date and time
import datetime
print(datetime.datetime.now())

This date time can be easily formatted using the special function from the date object “strftime”. With strftime you can convert the format of the time quite easily to a specified format of hours, mins, seconds and date, with or without the timezone information
import datetime
currentTime = datetime.datetime.now()
print(currentTime.strftime("%Y-%m-%d"))
print(currentTime.strftime("%Y-%m-%d %H:%M:%S"))
print(currentTime.strftime("%Y-%m-%d %H:%M:%S %Z%z"))

As you can guess the Y=year, m = month, d = year, H = hour, M=minutes, S = seconds. Z = timezone. You’ll notice that for the 3rd print item the timezone is blank. We’ll address that in the next section.
Print the time in the correct timezone
However, if you are using a remote machine, or a virtual machine where your local timezone is not set, you may want to chose your own timezone. Also, if you are running services which are across different machines, it is important to make sure you use the right timezone. One simple way is to use universal time (UTC), or to simply to set a single timezone. You can then convert as required.
import datetime
import pytz #include the timezone module
currentTime = datetime.datetime.now( pytz.timezone('UTC') )
print(currentTime.strftime("%Y-%m-%d"))
print(currentTime.strftime("%Y-%m-%d %H:%M:%S"))
print(currentTime.strftime("%Y-%m-%d %H:%M:%S %Z%z"))

Please note in the above example that when the timezone format was shown it showed that the timezone was set to UTC+0000 unlike the previous example. This means that the timezone information was present there.
In the following code, we will first get the time in the UTC timezone, and then convert the time to Hong Kong timezone.
import datetime
import pytz
currentTime = datetime.datetime.now( pytz.timezone('UTC') )
print("Time 1 (UTC time):", currentTime.strftime("%Y-%m-%d %H:%M:%S %Z%z"))
now_local = currentTime.astimezone(pytz.timezone('Asia/Hong_Kong'))
print("Time 2a(HK time) :", now_local.strftime("%Y-%m-%d %H:%M:%S %Z%z"))
print("Time 2b(HK time) :", now_local.strftime("%Y-%m-%d %H:%M:%S "))

Please note that in the “Time 2a” output, you can see the Hong Kong time as 2am with the timezone indicator at the end of +8 hours. The final “Time 2b” is the same time without the timezone included.
Finally, you can get a list of all the timezones available with a quick check on the pytz module and checking “all_timezones”.
import pytz
for tz in pytz.all_timezones:
print(tz)

How to print an exception
Things will go wrong in your code all the time – especially things that you don’t expect. This is where exceptions come in where the try exception blocks fit in quite nicely. The tricky part is that you need to make sure you output what the exception is in order for you to understand what’s going on.
Firstly a quick example of where a try /except can be helpful. Suppose you had the following code where after the definition of the function, the function was called.
def badFunction():
print(a) #print an undefined function
badFunction()#call the functionprint("have a nice day")

In here, as the variable “a” was not defined, then the program terminated and the final line “have a nice day” was never printed.
This is where try/except blocks can come in where you can catch errors from uncertain actions. So you can wrap the “badfunction” in a try block. See following example:
def badFunction():
print(a)
#Try the unsafe code
try:
badFunction()
except NameError:
print("Variable x is not defined")
except:
print("Something else went wrong")
print("have a nice day")

Here, the program continued to run gracefully and it caught the exception with the error “Variable x is not defined”. The reason it was caught was due to “NameError” exception object being defined.
In this next example, we have put a different error. Now the variable is defined as a number but there will be an exception as the number will be concatenated to a string.
def badFunction():
a = 1
print(a + ' join str') #this will fail as joining a string with a number
#try the unsafe code
try:
badFunction()
except NameError:
print("Variable x is not defined")
except:
print("Something else went wrong")
print("have a nice day")

Here another exception was caught but with the generic message of “Something else went wrong”. This is where printing the actual exception is really important. This is where you can define the exception object and print out the error.
ef badFunction():
a = 1
print(a + ' join str')
try:
badFunction()
except NameError:
print("Variable x is not defined")
except Exception as e:
print(e) #print the exception object print("have a nice day")

Here you can see that the reason for the failure was included, and the program continued to run.
There’s a final improvement we can make which is to include where the problem occurred. This is really important where you have logging defined and you can see where the issue was caused.
import traceback
def badFunction():
a = 1
print(a + ' join str')
#run the unsafe code
try:
badFunction()
except NameError:
print("Variable x is not defined")
except Exception as e:
print(e)
traceback.print_tb(e.__traceback__) #show the call list
print("have a nice day")

Here you can see the error description “Unsupported operand type(s) for +”, and then also where the error occurred from the initial call on line 8 with the call to “badFunction()” and the actual offending line of line 5.
Many more printing on python
There’s many more ways to print outputs within python, however this was intended to be a simple resource for some of the common printing challenges that come up, how you can use them, and with a simple example to get you up to speed very quickly with usable code. More to come!
Subscribe to our newsletter
How To Use Python orjson for Fast JSON Processing
Intermediate
You have a Python service that parses JSON responses from an API thousands of times per second, and the standard json module is quietly becoming a bottleneck. At low traffic volumes this goes unnoticed, but once you scale up, milliseconds of serialization overhead compound into real latency. If you have ever profiled a Python web service and found json.dumps or json.loads sitting near the top of the flame graph, you already know this pain.
orjson is a fast, correct JSON library for Python written in Rust. It drops into nearly any codebase as a replacement for the standard json module and typically runs 2-10x faster on both serialization and deserialization. It also natively supports types the standard library forces you to handle manually — datetime, UUID, numpy arrays, and dataclasses.
In this article you will learn how to install orjson, serialize and deserialize JSON with it, use its built-in support for Python-native types, benchmark it against the standard library, and integrate it into a real-world FastAPI project. By the end you will have a working understanding of when and why to choose orjson over the alternatives.
orjson Quick Example
Before diving deep, here is a self-contained example that shows the core pattern. orjson is nearly a drop-in replacement for the standard json module, but returns and accepts bytes instead of str.
# quick_example.py
import orjson
from datetime import datetime
data = {
"name": "Alice",
"score": 98.6,
"logged_in": True,
"joined": datetime(2024, 3, 15, 9, 30, 0),
"tags": ["python", "backend","fast"]
}
# Serialize to bytes (not str like the standard json module)
encoded = orjson.dumps(data)
print(encoded)
print(type(encoded))
# Deserialize back to a Python dict
decoded = orjson.loads(encoded)
print(decoded["joined"]) # datetime is serialized as ISO 8601 string
print(type(decoded))
Output:
b'{"name":"Alice","score":98.6,"logged_in":true,"joined":"2024-03-15T09:30:00","tags":["python","backend","fast"]}'
<class 'bytes'>
2024-03-15T09:30:00
<class 'dict'>
Two things stand out right away. First, orjson.dumps() returns bytes, not a string — this is intentional and saves an unnecessary encoding step when writing to network sockets or files. Second, the datetime object is automatically serialized to ISO 8601 format without any extra work, which the standard json module would refuse to handle at all.
What Is orjson and Why Use It?
orjson is a Python JSON library implemented in Rust using the Serde framework. It was created specifically to address the performance limitations of Python’s built-in json module, which is implemented in C but still shows its age when processing large payloads at high throughput.
The key differences between orjson and the standard library are:
| Feature | Standard json | orjson |
|---|---|---|
| Output type of dumps() | str | bytes |
| datetime support | Raises TypeError | Native ISO 8601 |
| UUID support | Raises TypeError | Native string |
| dataclass support | Raises TypeError | Native dict-like |
| numpy array support | Not supported | Native (optional dep) |
| Performance (typical) | Baseline | 2-10x faster |
| Strict UTF-8 validation | No | Yes |
The Rust implementation takes advantage of SIMD instructions and a highly optimized Serde-based serialization pipeline. For applications doing heavy JSON processing — API gateways, caching layers, log aggregators — the improvement is measurable and often significant.
Installing orjson
orjson is available on PyPI and installs with a single command:
# install_orjson.sh
pip install orjson
Output:
Collecting orjson
Downloading orjson-3.10.x-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (144 kB)
Successfully installed orjson-3.10.x
orjson ships as a pre-compiled binary for most platforms (Linux, macOS, Windows on x86-64 and ARM), so there is no Rust toolchain required. If you are on a less common platform you may need Rust installed to build from source. Verify the installation with a quick import check:
# verify_install.py
import orjson
print(orjson.__version__)
Output:
3.10.x
Serializing Python Objects with orjson.dumps()
The orjson.dumps() function converts Python objects to JSON bytes. The most important thing to remember is that it always returns bytes, not str. If you need a string, call .decode() on the result.
# serialization_basics.py
import orjson
from datetime import datetime, date
from uuid import UUID
from dataclasses import dataclass
@dataclass
class User:
id: UUID
name: str
created: datetime
active: bool
user = User(
id=UUID("12345678-1234-5678-1234-567812345678"),
name="Bob Smith",
created=datetime(2025, 1, 10, 14, 30),
active=True
)
# Serialize the dataclass directly -- no custom encoder needed
result = orjson.dumps(user)
print(result)
# Decode to string if needed
print(result.decode("utf-8"))
Output:
b'{"id":"12345678-1234-5678-1234-567812345678","name":"Bob Smith","created":"2025-01-10T14:30:00","active":true}'
{"id":"12345678-1234-5678-1234-567812345678","name":"Bob Smith","created":"2025-01-10T14:30:00","active":true}
Notice that the UUID, datetime, and dataclass are all handled automatically with zero configuration. With the standard json module, each of these would raise a TypeError: Object of type X is not JSON serializable error, requiring a custom default function.
orjson Options and Flags
orjson supports serialization options passed via the option parameter as bitwise-OR combinations of constants. These let you control formatting, sorting, and type handling:
# orjson_options.py
import orjson
data = {
"z_key": "last",
"a_key": "first",
"count": 42,
"ratio": 3.14159
}
# Pretty-print with indented output
pretty = orjson.dumps(data, option=orjson.OPT_INDENT_2)
print("Pretty:")
print(pretty.decode())
# Sort keys alphabetically
sorted_output = orjson.dumps(data, option=orjson.OPT_SORT_KEYS)
print("\nSorted keys:")
print(sorted_output.decode())
# Combine options with bitwise OR
both = orjson.dumps(data, option=orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS)
print("\nPretty + Sorted:")
print(both.decode())
Output:
Pretty:
{
"z_key": "last",
"a_key": "first",
"count": 42,
"ratio": 3.14159
}
Sorted keys:
{"a_key":"first","count":42,"ratio":3.14159,"z_key":"last"}
Pretty + Sorted:
{
"a_key": "first",
"count": 42,
"ratio": 3.14159,
"z_key": "last"
}
The most useful options in practice are OPT_INDENT_2 for human-readable output during debugging, OPT_SORT_KEYS for deterministic output in tests or caches, OPT_NON_STR_KEYS for dicts with integer or float keys, and OPT_UTC_Z to use Z suffix instead of +00:00 for UTC datetimes.
Deserializing with orjson.loads()
The orjson.loads() function accepts both bytes and str input and returns Python objects. Unlike the standard library, it performs strict UTF-8 validation on input, which means malformed data fails loudly rather than silently corrupting your data.
# deserialization.py
import orjson
# From bytes (most common in API and network scenarios)
json_bytes = b'{"name": "Charlie", "score": 99.5, "tags": ["fast", "correct"]}'
data = orjson.loads(json_bytes)
print(data)
print(type(data["score"]))
# From string also works
json_str = '{"status": "ok", "count": 1000}'
data2 = orjson.loads(json_str)
print(data2)
# Error handling -- orjson raises JSONDecodeError for invalid input
try:
orjson.loads(b'{"broken": }')
except orjson.JSONDecodeError as e:
print(f"Parse error: {e}")
Output:
{'name': 'Charlie', 'score': 99.5, 'tags': ['fast', 'correct']}
<class 'float'>
{'status': 'ok', 'count': 1000}
Parse error: expected value at line 1 column 12
One important detail: orjson.JSONDecodeError is a subclass of json.JSONDecodeError, so any existing except blocks using json.JSONDecodeError will still catch orjson errors without modification. This makes the migration path from the standard library seamless.
Benchmarking orjson vs Standard json
Let us run a concrete benchmark so you can see the actual performance difference on your hardware. We test serializing and deserializing a moderately complex nested dictionary 100,000 times:
# benchmark_orjson.py
import json
import orjson
import time
from datetime import datetime
# Test data -- similar to a typical API response
sample_data = {
"users": [
{"id": i, "name": f"User{i}", "email": f"user{i}@example.com",
"score": i * 1.5, "active": i % 2 == 0, "tags": ["python", "backend"]}
for i in range(50)
],
"total": 50,
"page": 1
}
ITERATIONS = 100_000
# Benchmark json.dumps
start = time.perf_counter()
for _ in range(ITERATIONS):
json.dumps(sample_data)
json_dumps_time = time.perf_counter() - start
# Benchmark orjson.dumps (returns bytes)
start = time.perf_counter()
for _ in range(ITERATIONS):
orjson.dumps(sample_data)
orjson_dumps_time = time.perf_counter() - start
# Benchmark json.loads
json_str = json.dumps(sample_data)
start = time.perf_counter()
for _ in range(ITERATIONS):
json.loads(json_str)
json_loads_time = time.perf_counter() - start
# Benchmark orjson.loads
orjson_bytes = orjson.dumps(sample_data)
start = time.perf_counter()
for _ in range(ITERATIONS):
orjson.loads(orjson_bytes)
orjson_loads_time = time.perf_counter() - start
print(f"json.dumps: {json_dumps_time:.3f}s")
print(f"orjson.dumps: {orjson_dumps_time:.3f}s ({json_dumps_time/orjson_dumps_time:.1f}x faster)")
print(f"json.loads: {json_loads_time:.3f}s")
print(f"orjson.loads: {orjson_loads_time:.3f}s ({json_loads_time/orjson_loads_time:.1f}x faster)")
Output (typical results on a modern CPU):
json.dumps: 2.841s
orjson.dumps: 0.482s (5.9x faster)
json.loads: 2.103s
orjson.loads: 0.631s (3.3x faster)
Actual speedups vary based on payload size, nesting depth, and hardware, but 3-6x faster on both operations is typical. For a service handling 1,000 requests per second with 100KB payloads each, this translates to substantial CPU savings that compound at scale.
Real-Life Example: FastAPI Response Caching with orjson
Here is a practical example that integrates orjson into a FastAPI application. We use orjson for both serializing API responses and caching them in memory, demonstrating a common production pattern:
# fastapi_orjson_cache.py
"""
FastAPI app with orjson-powered response serialization and in-memory caching.
Run with: uvicorn fastapi_orjson_cache:app --reload
"""
import orjson
from fastapi import FastAPI
from fastapi.responses import Response
from datetime import datetime, timezone
from dataclasses import dataclass, field
from typing import Optional
import hashlib
app = FastAPI()
# Simple in-memory cache using orjson bytes as values
_cache: dict[str, bytes] = {}
@dataclass
class ProductRecord:
id: int
name: str
price: float
in_stock: bool
last_updated: datetime
tags: list[str] = field(default_factory=list)
def get_product_from_db(product_id: int) -> Optional[ProductRecord]:
"""Simulates a database lookup."""
if product_id > 100:
return None
return ProductRecord(
id=product_id,
name=f"Product {product_id}",
price=round(product_id * 9.99, 2),
in_stock=product_id % 3 != 0,
last_updated=datetime.now(timezone.utc),
tags=["electronics", "featured"] if product_id < 50 else ["clearance"]
)
@app.get("/products/{product_id}")
async def get_product(product_id: int):
cache_key = f"product:{product_id}"
# Check cache first
if cache_key in _cache:
# Return cached bytes directly -- no re-serialization needed
return Response(content=_cache[cache_key], media_type="application/json")
product = get_product_from_db(product_id)
if product is None:
error = orjson.dumps({"error": "Product not found", "id": product_id})
return Response(content=error, media_type="application/json", status_code=404)
# Serialize with orjson -- handles dataclass and datetime natively
encoded = orjson.dumps(product, option=orjson.OPT_INDENT_2)
_cache[cache_key] = encoded
return Response(content=encoded, media_type="application/json")
@app.get("/cache/stats")
async def cache_stats():
stats = {
"cached_keys": len(_cache),
"cache_size_bytes": sum(len(v) for v in _cache.values()),
"timestamp": datetime.now(timezone.utc)
}
return Response(content=orjson.dumps(stats), media_type="application/json")
Example curl output:
$ curl http://localhost:8000/products/42
{
"id": 42,
"name": "Product 42",
"price": 419.58,
"in_stock": true,
"last_updated": "2025-03-15T10:22:41.123456+00:00",
"tags": ["electronics", "featured"]
}
The power here is that the serialized bytes are stored in the cache and served directly as the HTTP response body without deserialization or re-serialization. orjson's native datetime handling means the UTC-aware datetime in last_updated is serialized to a full ISO 8601 string with timezone offset -- exactly what frontend clients expect.
Frequently Asked Questions
Why does orjson return bytes instead of str?
orjson returns bytes because JSON data in Python is almost always immediately encoded to bytes for network transport or file writing. Returning bytes directly avoids an extra .encode("utf-8") step. If you need a string, just call result.decode(). This is a deliberate performance decision -- the bytes representation is the final form that gets sent over the wire.
Is orjson a drop-in replacement for the json module?
Almost, but not completely. The function signatures are similar, but orjson.dumps() returns bytes while json.dumps() returns str. Any code that does f.write(json.dumps(data)) will break because you cannot write bytes to a text-mode file. The fix is either f.write(orjson.dumps(data).decode()) or opening the file in binary mode "wb". The default= parameter also works slightly differently in edge cases.
How do I serialize custom types that orjson doesn't support natively?
Use the default parameter with a callback function, just like the standard library. The function receives the object and should return a JSON-serializable value. For example, to serialize a Decimal: orjson.dumps(data, default=lambda x: float(x) if isinstance(x, Decimal) else TypeError). orjson's native type support is broad enough that custom default handlers are rarely needed for modern Python code.
Is orjson thread-safe?
Yes. orjson functions are stateless -- each call to dumps() or loads() is entirely independent. There is no global mutable state, so multiple threads can call orjson simultaneously without any synchronization. This makes it a natural fit for multi-threaded web servers like gunicorm or uvicorn workers.
How does orjson compare to ujson?
Both are faster than the standard library, but orjson is consistently faster than ujson in benchmarks and has better correctness guarantees. ujson has a history of silently dropping or corrupting data in edge cases (very large integers, NaN values, deeply nested structures). orjson prioritizes correctness alongside speed. For production code where data integrity matters, orjson is the better choice.
Conclusion
orjson delivers a simple, high-value upgrade to any Python codebase that does significant JSON processing. The Rust-based implementation provides 3-6x faster serialization and deserialization, native support for datetime, UUID, dataclasses, and numpy arrays, and correct strict UTF-8 validation -- all with an API close enough to the standard library that migration is usually a matter of replacing the import and handling the bytes return type.
Try extending the FastAPI caching example to use Redis as a backend instead of in-memory storage, or add a Cache-Control header to the response based on the product's last_updated timestamp. These are natural next steps that reinforce how orjson fits into production API patterns.
For the full API reference and advanced options like OPT_PASSTHROUGH_DATETIME, see the orjson GitHub repository.
Related Articles
Further Reading: For more details, see the Python print() function documentation.
Frequently Asked Questions
What does \n do in Python print statements?
The \n escape sequence creates a newline character, causing text after it to appear on the next line. For example, print('Hello\nWorld') outputs ‘Hello’ and ‘World’ on separate lines.
How do I print multiple lines without using \n?
You can use triple-quoted strings (''' or """) to write multi-line text directly, or call print() multiple times. The textwrap.dedent() function also helps format multi-line strings cleanly.
What is a format exception in Python?
A format exception (typically a ValueError) occurs when a format string and its arguments do not match. For example, using the wrong number of placeholders in str.format() or mismatched types in f-strings.
How do I use f-strings for text formatting in Python?
F-strings (formatted string literals) use the syntax f'text {variable}' and were introduced in Python 3.6. They allow you to embed expressions directly inside string literals for readable, efficient formatting.
What is the difference between print() and sys.stdout.write()?
print() adds a newline by default and accepts multiple arguments with separators. sys.stdout.write() writes raw text without any automatic newline, giving you more control over output formatting.