Q# Beginner Free 1/7 in series 30 min read

Getting Started with Q#

Learn Microsoft's Q# quantum programming language, a purpose-built language for quantum algorithms with built-in classical-quantum integration and Azure Quantum access.

What you'll learn

  • qsharp
  • q#
  • microsoft quantum
  • quantum programming
  • .net

Prerequisites

  • Basic Python (variables, functions, loops)
  • No quantum physics background needed

Why Q# Is Different

Q# (Q-sharp) is Microsoft’s quantum programming language, and it occupies a unique position in the quantum computing landscape: it is a domain-specific language (DSL), not a library. In Qiskit and Cirq, you import a Python module and call functions that build circuits. In Q#, you write a program in a purpose-built language with its own syntax, type system, and compilation model.

This distinction has real consequences. Because Q# controls the entire language, its compiler can enforce constraints that Python simply cannot express. For example, Q# requires that all qubits are returned to the |0⟩ state before deallocation. If your code violates this rule, the compiler or runtime catches it, not a unit test you remembered to write. The compiler can also reason about quantum operations in ways that are impossible in a general-purpose language: it can automatically generate the inverse of any operation, verify that functions have no quantum side effects, and optimize gate sequences during compilation.

Q# integrates with Python for hybrid programs. You write your quantum operations in Q#, then call them from Python for classical pre-processing, post-processing, and visualization. This gives you the best of both worlds: a language designed for quantum correctness, with access to the entire Python data science ecosystem.

Installation

To get started, you need the .NET SDK (version 6+) and the Quantum Development Kit (QDK).

The QDK is Microsoft’s full toolchain for quantum programming. It includes the Q# compiler, the quantum runtime, a local simulator, resource estimation tools, and Azure Quantum integration. Think of it as the complete development environment for quantum programs, similar to what the JDK is for Java or what the Go toolchain is for Go.

# Install the QDK project templates
dotnet new install Microsoft.Quantum.ProjectTemplates

The dotnet templates give you project scaffolding, similar to create-react-app for web development or cargo new for Rust. They generate a project file, a main Q# source file, and the build configuration you need to compile and run.

For Python integration, install the qsharp package. This lets you run Q# code from a Python host, which is the most common workflow for data scientists and researchers who want quantum operations embedded in a larger classical pipeline.

# For Python integration
pip install qsharp azure-quantum

Verify your installation:

python -c "import qsharp; print('qsharp ready')"

Q# as a Language

Q# is not Python with quantum keywords bolted on. It is a statically typed, compiled language with features specifically designed for quantum programming. Understanding these language features is essential before writing your first program.

Static Typing

Every variable in Q# has a type that the compiler checks before your code runs. The core types include Int, Double, Bool, String, Qubit, Result (which holds measurement outcomes: Zero or One), and various array and tuple types.

// Q# has dedicated quantum types
let count : Int = 10;          // immutable integer
mutable flag : Bool = true;    // mutable boolean
// Qubit is special: you cannot create one with a literal,
// you must allocate it with `use`

This static typing catches errors at compile time. If you accidentally pass an Int where a Double is expected, or try to apply a gate to a Result instead of a Qubit, the compiler tells you immediately.

The use Statement

The use statement is how you allocate qubits. It guarantees that every qubit starts in the |0⟩ state and that the qubit is automatically deallocated when the use block exits. Critically, Q# verifies that the qubit is back in |0⟩ at deallocation. If it is not, you get a runtime exception.

// Allocate a single qubit
use q = Qubit();
// q is guaranteed to be in |0⟩ here
H(q);
// ... do quantum work ...
Reset(q);  // must return to |0⟩ before the block ends

// Allocate multiple qubits
use (q0, q1) = (Qubit(), Qubit());

// Allocate an array of qubits
use register = Qubit[5];

This is fundamentally different from Qiskit, where you create a QuantumRegister and nothing prevents you from leaving qubits in arbitrary states. Q#‘s approach reflects a real hardware constraint: on many platforms, qubits are a shared resource that must be returned to a known state for reuse.

Common Gates and Measurements

Quantum gates in Q# are operations, not methods on a circuit object. You call them directly on qubits.

H(q);           // Hadamard: creates superposition
X(q);           // Pauli-X (bit flip, equivalent to NOT)
Z(q);           // Pauli-Z (phase flip)
T(q);           // T gate (pi/8 phase rotation)
CNOT(q0, q1);   // Controlled-NOT: flips q1 if q0 is |1⟩

For measurements, Q# offers two main options:

  • M(q) measures the qubit in the computational (Z) basis and returns a Result. The qubit remains in the post-measurement state.
  • MResetZ(q) measures the qubit and then resets it to |0⟩. This is usually what you want, because it satisfies the deallocation requirement.

The within { } apply { } Pattern

This is one of Q#‘s most distinctive features. The within/apply construct performs a conjugation: it runs block A, then block B, then automatically runs the inverse of block A.

within {
    // Block A: setup operations
} apply {
    // Block B: the core computation
}
// The compiler automatically inserts Adjoint(Block A) here

This pattern appears constantly in quantum algorithms. Any time you need to transform into a different basis, perform an operation, and transform back, within/apply handles the “transform back” step automatically. This eliminates an entire class of bugs where you forget to invert a step or invert it incorrectly.

Functors: Adjoint and Controlled

Q# can automatically generate two variants of any operation:

  • Adjoint produces the inverse (dagger) of the operation. Adjoint T(q) gives the T-dagger gate. Adjoint H(q) is equivalent to H(q) because the Hadamard is self-adjoint.
  • Controlled produces the controlled version. Controlled X([control], target) is a CNOT gate.

These functors compose: Controlled Adjoint S([c], t) gives you a controlled S-dagger gate without writing any extra code.

repeat-until-success

Q# has a built-in loop construct for probabilistic quantum algorithms, where you run an operation, measure to check if it succeeded, and retry if it did not.

mutable result = One;
repeat {
    // Attempt the probabilistic operation
    use ancilla = Qubit();
    H(ancilla);
    // ... quantum operations ...
    set result = MResetZ(ancilla);
}
until result == Zero;

This replaces awkward while-loop patterns that you would write in Python-based frameworks.

Your First Q# Program: A Bell State

Create a file named BellState.qs. This program creates a Bell state, which is the simplest example of quantum entanglement: two qubits that are perfectly correlated.

namespace BellState {
    open Microsoft.Quantum.Canon;
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Measurement;

    @EntryPoint()
    operation CreateBellState() : (Result, Result) {
        use (q0, q1) = (Qubit(), Qubit());

        // Create Bell state |Φ+⟩
        H(q0);
        CNOT(q0, q1);

        // Measure and return results
        let (r0, r1) = (MResetZ(q0), MResetZ(q1));
        return (r0, r1);
    }
}

Here is what each part does:

  • namespace BellState groups the code into a named scope, similar to namespaces in C# or packages in Java.
  • open Microsoft.Quantum.Intrinsic imports the standard gate operations (H, X, CNOT, etc.). open Microsoft.Quantum.Measurement imports measurement operations like MResetZ.
  • @EntryPoint() marks this operation as the program’s main entry point, like main() in C or if __name__ == "__main__" in Python.
  • operation CreateBellState() : (Result, Result) declares a quantum operation (not a function) that returns a tuple of two Result values.
  • use (q0, q1) = (Qubit(), Qubit()) allocates two fresh qubits, both guaranteed to be in |0⟩.
  • H(q0) applies the Hadamard gate, putting q0 into an equal superposition of |0⟩ and |1⟩.
  • CNOT(q0, q1) entangles the two qubits. The combined state is now (|00⟩ + |11⟩)/√2.
  • MResetZ(q0) measures q0 and resets it to |0⟩, satisfying the deallocation requirement. Same for q1.

Run it:

dotnet run
# (One, One) or (Zero, Zero) - always matching

You will always see both qubits return the same value. This is entanglement: measuring one qubit instantly determines the other, regardless of the physical distance between them.

The within/apply Pattern in Practice

To see why within/apply matters, consider a concrete problem: computing the parity (XOR) of three data qubits into an ancilla qubit. In many quantum algorithms, you need to compute a value, use it, and then uncompute it to clean up the ancilla.

The Q# Way

operation ComputeParityAndUse(data : Qubit[], ancilla : Qubit, target : Qubit) : Unit {
    within {
        // Compute parity of data qubits into ancilla
        // CNOT each data qubit into ancilla: ancilla = XOR of all data qubits
        for q in data {
            CNOT(q, ancilla);
        }
    } apply {
        // Use the parity result: conditionally flip the target
        CNOT(ancilla, target);
    }
    // The compiler automatically uncomputes the parity:
    // it applies CNOT(data[2], ancilla), CNOT(data[1], ancilla),
    // CNOT(data[0], ancilla) in reverse order.
    // The ancilla is back in |0⟩ and safe to deallocate.
}

The within block computes the parity by CNOTing each data qubit into the ancilla. The apply block uses that parity. Then Q# automatically reverses the within block, uncomputing the parity and returning the ancilla to |0⟩.

The Equivalent in Qiskit (Manual Inversion)

In Qiskit, you must manually write the inverse operations:

from qiskit import QuantumCircuit

qc = QuantumCircuit(5)  # 3 data + 1 ancilla + 1 target
data = [0, 1, 2]
ancilla = 3
target = 4

# Compute parity into ancilla
for q in data:
    qc.cx(q, ancilla)

# Use the parity
qc.cx(ancilla, target)

# Manually uncompute parity (must reverse the order yourself)
for q in reversed(data):
    qc.cx(q, ancilla)

This works for a simple example, but in complex algorithms with dozens of intermediate computations, manually tracking which operations to invert and in what order is a major source of bugs. Q#‘s within/apply eliminates this problem entirely.

Functors: Adjoint and Controlled

Q# operations can declare support for Adjoint and Controlled functors, and the compiler generates the implementations automatically.

Adjoint (Inverse)

The Adjoint functor gives you the inverse of any operation. This is critical for algorithms like Quantum Phase Estimation (QPE), where you need the inverse Quantum Fourier Transform after the phase kickback step.

operation PrepareState(q : Qubit) : Unit is Adj {
    H(q);
    T(q);
    S(q);
}

operation Example(q : Qubit) : Unit {
    PrepareState(q);
    // ... do something ...
    Adjoint PrepareState(q);  // applies S†, T†, H in reverse order
}

The is Adj annotation tells Q# that this operation supports the Adjoint functor. The compiler generates Adjoint PrepareState by reversing the operation order and replacing each gate with its adjoint: S becomes S-dagger, T becomes T-dagger, and H remains H (since it is self-adjoint).

Controlled

The Controlled functor turns any single-qubit or multi-qubit operation into a controlled version.

operation MyRotation(q : Qubit) : Unit is Ctl {
    H(q);
    T(q);
}

operation Example(control : Qubit, target : Qubit) : Unit {
    // Apply MyRotation to target, controlled on control
    Controlled MyRotation([control], target);
}

In Qiskit, producing the controlled version of an arbitrary circuit requires decomposition and is not always straightforward. In Q#, it is a single keyword.

Comparison to Qiskit

In Qiskit, you generate the inverse of a circuit by calling .inverse() on the circuit object:

from qiskit import QuantumCircuit

qc = QuantumCircuit(1)
qc.h(0)
qc.t(0)
qc.s(0)

inverse_qc = qc.inverse()  # returns a new circuit: Sdg, Tdg, H

This works, but it is a runtime operation on a data structure. Q#‘s approach is a compile-time transformation on the operation itself, which means the compiler can verify correctness and optimize the result.

Using Q# from Python

The most common workflow is driving Q# from a Python host. This lets you use Python for data loading, classical optimization, and plotting while keeping your quantum logic in Q#.

The qsharp Python package provides two main entry points:

  • qsharp.eval() compiles and runs a Q# expression or block, returning the result to Python.
  • qsharp.run() executes a named Q# operation for a specified number of shots, returning all results.
import qsharp

# Inline Q# definition
result = qsharp.eval("""
    open Microsoft.Quantum.Intrinsic;
    open Microsoft.Quantum.Measurement;

    operation BellPair() : (Result, Result) {
        use (q0, q1) = (Qubit(), Qubit());
        H(q0);
        CNOT(q0, q1);
        return (MResetZ(q0), MResetZ(q1));
    }
""")

# Run 1000 shots
from collections import Counter
counts = Counter(qsharp.run("BellPair()", shots=1000))
print(counts)
# {(Zero, Zero): ~500, (One, One): ~500}

qsharp.eval() is useful for quick experiments and one-off computations. For production code, you typically define your Q# operations in .qs files and use qsharp.run() to execute them from Python with a specific shot count.

Running on Azure Quantum

Running quantum circuits on Azure Quantum provides access to various hardware backends. This platform allows developers to implement and test quantum algorithms using a unified interface, simplifying the process of deploying quantum code across different quantum processors.

import qsharp
from azure.quantum import Workspace

workspace = Workspace(
    resource_id="/subscriptions/.../Microsoft.Quantum/Workspaces/my-workspace",
    location="eastus"
)

# Target IonQ or Quantinuum hardware
target = workspace.get_targets("ionq.simulator")
job = target.submit(qsharp.compile("BellPair"), shots=500)
result = job.get_results()

SDK Comparison

The quantum programming landscape has three major frameworks, each with different design philosophies and hardware ecosystems. The table below summarizes the key differences.

FeatureQ#QiskitCirq
Language typeDomain-specific languagePython libraryPython library
Type safetyStatic (compile-time)Dynamic (runtime)Dynamic (runtime)
Hardware targetAzure QuantumIBM QuantumGoogle Sycamore
Adjoint generationAutomatic (compiler)Manual (circuit.inverse())Manual
Qubit managementEnforced by compilerManualManual
Learning curveSteeper (new language)Gentler (Python)Gentler (Python)
Best forAlgorithm design, formal verificationIBM hardware, educationGoogle hardware, NISQ research

The choice depends on your goals. If you value type safety, automatic adjoint generation, and compiler-enforced correctness, Q# is the strongest option. If you want the largest community and the easiest on-ramp, Qiskit’s Python-native approach wins. If you are targeting Google hardware or doing noise-aware NISQ research, Cirq is the natural fit.

Common Mistakes

Forgetting to Reset Qubits Before Deallocation

Q# requires that all qubits are in the |0⟩ state when a use block exits. If you measure a qubit with M() and it collapses to |1⟩, you must explicitly reset it before the block ends.

// This will throw a runtime exception if the measurement returns One
use q = Qubit();
H(q);
let result = M(q);
// Missing: Reset(q);
// The use block ends here, and q might be in |1⟩

Fix: use MResetZ(q) instead of M(q) when you want to measure and reset in one step, or call Reset(q) after M(q).

Trying to Copy or Clone a Qubit

The no-cloning theorem states that you cannot create an independent copy of an unknown quantum state. Q# enforces this through its type system. You cannot assign one qubit to another or pass qubits by value.

// This does not work: you cannot clone a qubit
use q1 = Qubit();
H(q1);
// let q2 = q1;  // Compile error: qubits cannot be copied

If you need to “copy” classical information from a qubit, measure it first. If you need to entangle two qubits so they share a state, use CNOT or other entangling gates.

Using M When You Need MResetZ

M(q) measures the qubit but leaves it in the post-measurement state (|0⟩ or |1⟩). MResetZ(q) measures the qubit and then resets it to |0⟩. In most cases, you want MResetZ because it satisfies the deallocation requirement automatically.

Use M(q) only when you need the qubit to remain in its post-measurement state for further operations, such as mid-circuit measurement and conditional logic.

Confusing Operations with Functions

Q# distinguishes between operations and functions. Operations can have quantum effects (allocate qubits, apply gates, perform measurements). Functions are purely classical and cannot touch qubits.

// This is an operation: it can apply quantum gates
operation ApplyHadamard(q : Qubit) : Unit {
    H(q);
}

// This is a function: it can only do classical computation
function Square(x : Int) : Int {
    return x * x;
}

If you try to call a gate inside a function, the compiler rejects it. This separation helps the compiler reason about which parts of your code have quantum effects and which are purely classical.

Next Steps

Now that you understand Q#‘s core features, here are productive directions to explore:

  • Q# Quantum Katas: Microsoft’s Quantum Katas are interactive exercises that teach quantum concepts through Q# programming challenges. They cover everything from basic gates to Deutsch-Jozsa and Simon’s algorithms.
  • Grover’s Search in Q#: Implementing Grover’s algorithm in Q# is one of the cleanest implementations in any framework. The within/apply pattern handles the oracle conjugation naturally, and the Controlled functor simplifies multi-controlled gate construction.
  • Quantum Phase Estimation: QPE is a foundational subroutine for Shor’s algorithm and quantum chemistry. Implementing it in Q# demonstrates the power of the Adjoint functor, since you need the inverse QFT as a key step.
  • Azure Quantum resource estimation: Before running on real hardware, use Q#‘s resource estimator to understand the qubit count and gate depth your algorithm requires. This helps you evaluate whether your algorithm is feasible on current or near-term devices.

Was this tutorial helpful?