Intermediate

I/O Operations in Nim

Learn console input and output, file reading and writing, formatted output, and I/O error handling in Nim with Docker-ready examples

Input and output are how a program talks to the outside world: printing results, reading what the user types, and persisting data to files. You have already met echo in the Hello World tutorial, but Nim’s standard library offers far more control than that single procedure suggests.

Nim is a multi-paradigm systems language, and its I/O reflects that heritage. Console and file operations are ordinary procedures that work on File objects, giving you C-like control while keeping Python-like readability. Because these operations touch the outside world, they are not pure — this is where Nim’s compile-time effect system matters. You cannot call readFile from a procedure marked func (which is shorthand for {.noSideEffect.}), because the compiler tracks that I/O has side effects.

In this tutorial you will learn how to write to the console with fine-grained control, read input from standard input, read and write files, format output cleanly with std/strformat, and handle I/O failures with try/except. Every example runs unchanged inside the official nimlang/nim:alpine Docker image.

Console Output Beyond echo

echo is convenient but always appends a newline and always joins its arguments. When you need precise control over spacing and line breaks, reach for the stdout file handle directly.

Create a file named io_output.nim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# echo joins all its arguments and adds a trailing newline
echo "echo adds a newline and can join args: ", 42, " ", true

# stdout.write does NOT add a newline
stdout.write("No newline here... ")
stdout.write("continued on same line\n")

# writeLine adds its own newline for you
stdout.writeLine("writeLine adds its own newline")

# write accepts multiple arguments in one call
stdout.write("a", "b", "c", "\n")

Key points:

  • echo converts every argument with $ and separates nothing automatically — add spaces yourself.
  • stdout.write writes raw text with no newline, ideal for prompts.
  • stdout.writeLine is write plus a trailing newline.

Expected Output

echo adds a newline and can join args: 42 true
No newline here... continued on same line
writeLine adds its own newline
abc

Reading Input from the Console

To read what a user types, use readLine(stdin). Input always arrives as a string, so convert it with helpers from std/strutils such as parseInt.

Create a file named io_input.nim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import std/strutils

stdout.write("What is your name? ")
let name = readLine(stdin)

stdout.write("What is your favorite number? ")
let numText = readLine(stdin)
let num = parseInt(numText)

echo "Hello, ", name, "!"
echo "Your number doubled is ", num * 2

Because this program waits for interactive input, you can feed it values through a pipe so the run is reproducible. In Docker, pipe two lines into the program’s standard input:

1
printf 'Ada\n21\n' | docker run --rm -i -v $(pwd):/app -w /app nimlang/nim:alpine nim c -r io_input.nim

Expected Output

What is your name? What is your favorite number? Hello, Ada!
Your number doubled is 42

The two prompts appear back-to-back because the piped input is consumed instantly rather than typed by a human. Run it interactively (without the pipe) and you will see each prompt pause for your answer.

Reading and Writing Files

Nim offers both whole-file convenience procedures and line-oriented file handles. writeFile and readFile handle an entire file in one call, while open gives you a File you can append to or iterate over. Always close a file handle you open explicitly.

Create a file named io_files.nim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
let filename = "notes.txt"

# Write a whole string at once (creates or overwrites the file)
writeFile(filename, "First line\nSecond line\n")

# Append with an explicit file handle opened in append mode
let f = open(filename, fmAppend)
f.writeLine("Third line (appended)")
f.close()

# Read the entire file back as a single string
let contents = readFile(filename)
echo "--- Full contents ---"
echo contents

# Read the file one line at a time (newlines are stripped)
echo "--- Line by line ---"
for line in lines(filename):
  echo "> ", line

Key points:

  • writeFile / readFile are the simplest way to move an entire file in or out of memory.
  • open(path, fmAppend) opens for appending; other modes include fmRead, fmWrite, and fmReadWrite.
  • lines(path) is an iterator that yields each line without its terminating newline — perfect for processing text files.

Expected Output

--- Full contents ---
First line
Second line
Third line (appended)

--- Line by line ---
> First line
> Second line
> Third line (appended)

The blank line after the full contents comes from the file’s own trailing newline plus the newline echo adds.

Formatted Output with strformat

For readable, aligned, and precisely formatted output, the std/strformat module provides fmt string interpolation — similar to Python’s f-strings but resolved at compile time.

Create a file named io_format.nim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import std/strformat

let name = "Nim"
let version = 2.2
let count = 7
let pi = 3.14159

# Interpolate variables directly into the string
echo fmt"Language: {name}, version {version}"

# Left-align a label to width 12, right-align a number to width 4
let label = "Widgets"
echo fmt"{label:<12}| {count:>4}"

# Floating point with two decimal places
echo fmt"Pi to 2 dp: {pi:.2f}"

# The same integer in hexadecimal and binary
echo fmt"{count} in hex = {count:x}, in binary = {count:b}"

Key points:

  • fmt"..." interpolates any expression inside {} and is evaluated when the program compiles.
  • {value:<12} and {value:>4} control alignment and field width.
  • {pi:.2f}, {count:x}, and {count:b} apply precision, hex, and binary format specifiers.

Expected Output

Language: Nim, version 2.2
Widgets     |    7
Pi to 2 dp: 3.14
7 in hex = 7, in binary = 111

Handling I/O Errors

File operations can fail — a path may not exist, or you may lack permission. Nim signals these problems by raising IOError, which you catch with try/except. This keeps error handling explicit and lets a program recover gracefully instead of crashing.

Create a file named io_errors.nim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import std/strutils

proc readConfig(path: string): string =
  try:
    result = readFile(path)
  except IOError as e:
    echo "Could not read ", path, ": ", e.msg
    result = ""

# This file does not exist, so readFile raises IOError
let missing = readConfig("does_not_exist.txt")
echo "Length of missing file contents: ", missing.len

# Create a file, then read it back successfully
writeFile("config.txt", "setting=on\n")
let config = readConfig("config.txt")
echo "Config: ", config.strip()

Key points:

  • try / except IOError as e catches the failure and exposes the message via e.msg.
  • result is Nim’s implicit return variable — assigning to it sets the procedure’s return value.
  • strip() from std/strutils removes the trailing newline before printing.

Expected Output

Could not read does_not_exist.txt: cannot open: does_not_exist.txt
Length of missing file contents: 0
Config: setting=on

Running with Docker

All five programs run with the same command pattern — swap in the filename you want to execute. The -r flag compiles to C and runs the resulting binary immediately.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official Nim image
docker pull nimlang/nim:alpine

# Run the console output example
docker run --rm -v $(pwd):/app -w /app nimlang/nim:alpine nim c -r io_output.nim

# Run the file read/write example
docker run --rm -v $(pwd):/app -w /app nimlang/nim:alpine nim c -r io_files.nim

# Run the formatted output example
docker run --rm -v $(pwd):/app -w /app nimlang/nim:alpine nim c -r io_format.nim

# Run the error handling example
docker run --rm -v $(pwd):/app -w /app nimlang/nim:alpine nim c -r io_errors.nim

For the interactive input example, add -i and pipe the answers in so the program has something to read:

1
printf 'Ada\n21\n' | docker run --rm -i -v $(pwd):/app -w /app nimlang/nim:alpine nim c -r io_input.nim

The volume mount (-v $(pwd):/app) is what lets file examples like io_files.nim write notes.txt and config.txt back to your current directory, where you can inspect them after the container exits.

Key Concepts

  • echo vs stdout.writeecho always adds a newline and joins arguments; stdout.write gives you raw, newline-free control ideal for prompts.
  • Input is always textreadLine(stdin) returns a string; convert it with parseInt, parseFloat, and friends from std/strutils.
  • Two file styles — use readFile/writeFile for whole files, or open/close with a File handle when you need appending or line-by-line iteration via lines().
  • File modes matterfmRead, fmWrite, fmAppend, and fmReadWrite decide what an opened file can do.
  • std/strformat — the fmt macro provides compile-time string interpolation with alignment, precision, hex, and binary specifiers.
  • I/O errors are exceptions — wrap risky file operations in try/except IOError and read the cause from e.msg.
  • Effect tracking — because I/O has side effects, these procedures cannot be called from a func; Nim’s compiler enforces the pure/impure boundary for you.
  • Always close what you open — explicit open handles should be paired with close to flush buffers and release the file.

Running Today

All examples can be run using Docker:

docker pull nimlang/nim:alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining