Skip to main content

This is a forked version of jinjasql, maintained at pythonutilities/jinjasql and published on PyPI as jinjasql2.

Generate SQL Queries using a Jinja Template, without worrying about SQL Injection

PyPI version Tests

JinjaSQL is a template language for SQL statements and scripts. Since it's based in Jinja2, you have all the power it offers - conditional statements, macros, looping constructs, blocks, inheritance, and many more.

JinjaSQL automatically binds parameters that are inserted into the template. After JinjaSQL evaluates the template, you get:

  1. A query with placeholders for the parameters
  2. The values corresponding to the placeholders that need to be bound to the query

JinjaSQL doesn't actually execute the query - it only prepares the query and the bind parameters. You can execute the query using any database engine / driver you are working with.

For example, if you have a template like this -

select username, sum(spend)
from transactions
where start_date > {{request.start_date}}
and end_date < {{request.end_date}}
{% if request.organization %}
and organization = {{request.organization}}
{% endif %}

then, depending on the parameters you provide, you get a query (shown here with param_style='format'; the default named style is covered below)

select username, sum(spend)
from transaction
where start_date > %s
and end_date < %s
and organization = %s

with bind parameters ('2016-10-10', '2016-10-20', 1321).

If request.organization was empty/falsy, the corresponding and clause would be absent from the query, and the bind parameters would not have the organization id.

When to use JinjaSQL

JinjaSQL is not meant to replace your ORM. ORMs like those provided by SQLAlchemy or Django are great for a variety of use cases, and should be the default in most cases. But there are a few use cases where you really need the power of SQL.

Use JinjaSQL for -

  1. Reporting, business intelligence or dashboard like use cases
  2. When you need aggregation/group by
  3. Use cases that require data from multiple tables
  4. Migration scripts & bulk updates that would benefit from macros

In all other use cases, you should reach to your ORM instead of writing SQL/JinjaSQL.

While JinjaSQL can handle insert/update statements, you are better off using your ORM to handle such statements. JinjaSQL is mostly meant for dynamic select statements that an ORM cannot handle as well.

A note on trust: JinjaSQL protects you from SQL injection through values - anything a {{ variable }} produces is bound, never inlined. The templates themselves must still be trusted: they are ordinary Jinja templates (evaluated without a sandbox) and the | sqlsafe filter lets a template inline anything verbatim. Treat templates as code written by developers, and context data as untrusted input.

Basic Usage

First, import the JinjaSql class and create an object. JinjaSql is thread-safe, so you can safely create one object at startup and use it everywhere. Just don't share the same Jinja Environment object across multiple JinjaSql instances - each instance configures the environment it is given.

from jinjasql import JinjaSql
j = JinjaSql()

Next, create your template query. You can use the full power of Jinja templates over here - macros, includes, imports, if/else conditions, loops, filters and so on. You can load the template from a file or from database or wherever else Jinja supports.

template = """
    SELECT project, timesheet, hours
    FROM timesheet
    WHERE user_id = {{ user_id }}
    {% if project_id %}
    AND project_id = {{ project_id }}
    {% endif %}
"""

Create a context object. This object is a regular dictionary, and can contain nested dictionaries, lists or objects. The template query is evaluated against this context object.

data = {
    "project_id": 123,
    "user_id": "sripathi"
}

Finally, call the prepare_query method with the template and the context. You get back two things:

  1. query is the generated SQL query. With the default param_style='named', variables are replaced by :name style placeholders
  2. bind_params is a dictionary of parameters corresponding to the placeholders
query, bind_params = j.prepare_query(template, data)

This is the query that is generated:

expected_query = """
    SELECT project, timesheet, hours
    FROM timesheet
    WHERE user_id = :user_id_1

    AND project_id = :project_id_2
"""

And these are the bind parameters:

self.assertEqual(bind_params, {"user_id_1": "sripathi", "project_id_2": 123})
self.assertEqual(query.strip(), expected_query.strip())

You can now use the query and bind parameters to execute the query. For example, in django, you would do something like this:

from django.db import connection
with connection.cursor() as cursor:
    cursor.execute(query, bind_params)
    for row in cursor.fetchall():
        # do something with the results
        pass

Multiple Param Styles

Per PEP-249, bind parameters can be specified in multiple ways. You can pass the optional constructor argument param_style to control the style of query parameter.

  1. format : ... where name = %s
  2. qmark : where name = ?
  3. numeric : where name = :1 and last_name = :2
  4. named : where name = :name and last_name = :last_name. This is the default.
  5. pyformat : where name = %(name)s and last_name = %(last_name)s
  6. asyncpg : where name = $1 and last_name = $2. This is not part of PEP-249 standard, but is used by asyncpg library for postgres

Here's how it works -

j = JinjaSql(param_style='named')
query, bind_params = j.prepare_query(template, data)

If param_style is named or pyformat, bind_params will be a python dictionary. For all other param styles, it will be a tuple.

In case of named and pyformat, remember the following:

  1. prepare_query returns a dictionary instead of a tuple
  2. The returned dictionary is flat, and only contains keys that are actually used in the query
  3. The keys in the dictionary and in the query are guaranteed to have unique names. Even if you bind the same parameter twice, the key will be renamed

Handling In Clauses

For SQL in clauses, you have to apply the | inclause filter, so that JinjaSQL creates one bind expression per element:

select 'x' from dual
where project_id in {{ project_ids | inclause }}

Notice that you don't need to enclose in parantheses.

The inclause filter expects a non-empty list or tuple; it raises a ValueError for an empty sequence (which would generate invalid SQL) or a plain string (which would bind one parameter per character).

Without the filter, a list or tuple is bound as a single parameter. That is intentional - drivers like psycopg2 and asyncpg accept python lists for postgres array columns (e.g. WHERE {{some_num}} = ANY({{some_array}})) - but it will not work as an in clause.

SQL Safe Strings

Sometimes, you want to insert dynamic table names/column names. By default, JinjaSQL will convert them to bind parameters. This won't work, because table and column names are usually not allowed in bind parameters.

In such cases, you can use the |sqlsafe filter.

select {{column_names | sqlsafe}} from dual

If you use sqlsafe, it is your responsibility to ensure there is no sql injection.

Alternatively, the |identifier filter can be used to produce escaped strings for safe usage of SQL identifiers such as table or column names. Pass a string for a plain identifier, or a tuple for a qualified one (e.g. schema and table):

template = """
select {{column1 | identifier}}, {{column2 | identifier}} from {{source | identifier}}
"""
j = JinjaSql()
query, bind_params = j.prepare_query(
    template, {'column1': 'col1', 'column2': 'col2', 'source': ('a_schema', 'a_table')}
)

Would result in the following query being rendered:

select "col1", "col2" from "a_schema"."a_table"

Identifiers are quoted with double quotes by default (ANSI SQL, postgres). For databases like MySQL that quote identifiers with backticks, use the optional constructor argument identifier_quote_character:

j = JinjaSql(identifier_quote_character='`')

Installing jinjasql

Pre-Requisites :

  1. python >= 3.8
  2. jinja2 >= 3.1.6 (installed automatically as a dependency)

To install from PyPi (recommended) :

pip install jinjasql2

Note: the package is published as jinjasql2, but it installs the jinjasql python module - don't install it side-by-side with the original jinjasql package.

How does JinjaSQL work?

The bind filter

At it's core, JinjaSQL provides a filter called bind. This filter gobbles up whatever value is provided, and emits a placeholder in its place. The actual value is then stored in a thread local list of bind parameters.

jinja.prepare_query("select * from user where id = {{userid | bind}}",
                    {"userid": 143})

When this code is evaluated, the output query is select * from user where id = %s (with param_style='format').

Pre-processing the Query Template

Manually applying the bind filter to every parameter is error-prone. Sooner than later, a developer will miss the filter, and it will lead to SQL Injection.

JinjaSQL automatically applies the bind filter to ALL variables. The query template is transformed before it is evaluated.

select * from user where id = {{userid}}

becomes

select * from user where id = {{userid | bind}}

Jinja lets extensions rewrite the token stream. JinjaSQL looks for variable_begin and variable_end tokens in the stream, and rewrites the stream to include the bind filter as the last filter.

Autoescape and JinjaSQL

Jinja has an autoescape feature. If turned on, it automatically HTML escapes variables. It does this by wrapping strings using the Markup class.

JinjaSQL builds on this functionality. JinjaSQL requires autoescape to be turned on. As a result, strings that are injected are wrapped using the Markup class. JinjaSQL uses this wrapper class as well to prevent double-binding of parameters.

License

jinjasql is licensed under the MIT License. See LICENSE.

(c) 2016 HashedIn Technologies Pvt. Ltd. (c) 2021 Sripathi Krishnan

Metadata

Release files for jinjasql2 0.1.13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for jinjasql2 0.1.13
File Size Uploaded
jinjasql2-0.1.13.tar.gz 14.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for jinjasql2 0.1.13
File Interpreter ABI Platform
jinjasql2-0.1.13-py3-none-any.whl Python 3 none any Details

Total release size: 24.0 kB

Release files / jinjasql2-0.1.13.tar.gz

Download URL jinjasql2-0.1.13.tar.gz
Size 14.4 kB
Tags Source
SHA-256 checksum
How to use checksums
c8c98a0be7608b945bad9a5239ddf1552683afa62dc3272f99770c2a7a650ba4
BLAKE2b-256 checksum
How to use checksums
85c0c3ad945c58bc59bbf11e8e95a9a84648178091de9e8b9f483f0742120160
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release files / jinjasql2-0.1.13-py3-none-any.whl

Download URL jinjasql2-0.1.13-py3-none-any.whl
Size 9.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ba64c4290ae4c11f8e5f5dbac0cf6fb55fb31a2b6da09af90738b9db2bd4a060
BLAKE2b-256 checksum
How to use checksums
75237f01df8fc688293c6d8551f3f13ed576e642fb45cad5174549d9d79d197b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 7, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.13 This release

2 release files

0.1.12

2 release files

0.1.11

2 release files

0.1.10

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page