Usage guide

To use akismet, you will need to obtain an Akismet API key and register a site for use with the Akismet web service; you can do this at <https://akismet.com>. Once you have a key and corresponding registered site URL to use with it, place them in the environment variables PYTHON_AKISMET_API_KEY and PYTHON_AKISMET_BLOG_URL, and they will be automatically detected and used.

You can also optionally set the environment variable PYTHON_AKISMET_TIMEOUT to a float or int containing a connection-timeout threshold to use for making requests to the Akismet web service; if not set, this will default to 1.0 (one second).

Using the Akismet clients

Once you have a key and registered site, and have set the environment variables, you can create an Akismet API client. Two are available, one being synchronous (blocking I/O), and the other asynchronous (non-blocking I/O).

Asynchronous Python

Most Python applications are synchronous, and cannot easily run async code (which requires an event loop and slightly different syntax to call functions/methods). So you’ll probably want to use the synchronous Akismet client unless your entire application is already async (most commonly, this will be when you have an asynchronous web application built with an async framework like FastAPI or Litestar).

And if you’re not sure what all this means, you definitely want the synchronous Akismet client.

API client creation and basic use

Generally you will want to create your client in a way which automatically validates your API key at client instantiation. You can do this in either of two ways:

  • Call the validated_client() constructor method (see the FAQ for an explanation of why this is done through an alternate constructor), or

  • Create your client as a context manager

To use the validated_client() constructor, simply call it. With no arguments, it will read your Akismet API key and site URL from the environment variables, and will raise an akismet.ConfigurationError exception if they’re not found or not valid.

import akismet

akismet_client = akismet.SyncClient.validated_client()
import akismet

akismet_client = await akismet.AsyncClient.validated_client()

If don’t want to or can’t set the environment variables you can use a Config instance and use that to manually configure your Akismet API client:

import akismet

config = akismet.Config(key=your_api_key, url=your_site_url)
akismet_client = akismet.SyncClient.validated_client(config=config)
import akismet

config = akismet.Config(key=your_api_key, url=your_site_url)
akismet_client = await akismet.AsyncClient.validated_client(config=config)

The most important operation of the Akismet client is checking a piece of content to see if it’s spam. This is done with the comment_check() method. There’s one required argument – the IP address of the user who submitted the content – but a large number of optional arguments are also accepted. It’s recommended that you include as much information as possible to help Akismet make accurate determinations, but at the very least you should pass the following arguments:

  • comment_content – The actual content that was submitted.

  • comment_type – The type of content. Common values for this are "comment", "forum-post", "contact-form", and "signup", but you can also pass other values depending on the type of user-submitted content you’re dealing with.

  • comment_author and/or comment_author_email – The identifier (such as a username) and/or the email address of the user who submitted the content.

For example, suppose you’re using the Django web framework to build an online forum. You might write a Django view for submitting new forum posts that looks like this (using the API client created above):

def new_post(request):
    """
    HTTP handler for a new forum post.

    """
    if akismet_client.comment_check(
        user_ip=request.META["REMOTE_ADDR"],
        comment_type="forum_post",
        comment_content=request.POST["post_body"],
        comment_author=request.user.username,
    ):
        # The post was spam, reject it.
    else:
        # The post wasn't spam, allow it.
async def new_post(request):
    """
    HTTP handler for a new forum post.

    """
    if await akismet_client.comment_check(
        user_ip=request.META["REMOTE_ADDR"],
        comment_type="forum_post",
        comment_content=request.POST["post_body"],
        comment_author=request.user.username,
    ):
        # The post was spam, reject it.
    else:
        # The post wasn't spam, allow it.

As a context manager

You can also use either client as a context manager. When doing so, you do not need to use the validated_client() constructor; the context manager will automatically validate the configuration for you as soon as the with block is entered.

import akismet

with akismet.SyncClient() as akismet_client:
    # Use the client instance here. It will be automatically cleaned up
    # when the "with" block is exited.
import akismet

async with akismet.AsyncClient() as akismet_client:
    # Use the client instance here. It will be automatically cleaned up
    # when the "with" block is exited.

As with the validated_client() method, you can explicitly pass a Config instance to the constructor to manually supply the API key and site URL.

Detecting “blatant” spam

The examples above showed spam detection as an either/or check – either something is spam, or it’s not. But Akismet actually supports three possible states: “not spam”, “spam”, and “blatant spam”. One way you could use this is to add a manual review step: if something is marked as “not spam” it’s allowed to post normally, “spam” goes into a review queue for you to look at, and “blatant spam” is just rejected without any further review.

You can implement this by looking at the return value of the comment_check() method, which is actually an enum – akismet.CheckResponse – with three possible values. So you could adapt the example of comment_check() above to do this:

from akismet import CheckResponse

def new_post(request):
    """
    HTTP handler for a new forum post.

    """
    classification = akismet_client.comment_check(
        user_ip=request.META["REMOTE_ADDR"],
        comment_type="forum_post",
        comment_content=request.POST["post_body"],
        comment_author=request.user.username,
    )

    if classification == CheckResponse.DISCARD:
        # The post was "blatant" spam, reject it.
    elif classification == CheckResponse.SPAM:
        # Send it into the manual-review queue.
    elif classification == CheckResponse.HAM:
        # The post wasn't spam, allow it.
from akismet import CheckResponse

async def new_post(request):
    """
    HTTP handler for a new forum post.

    """
    classification = await akismet_client.comment_check(
        user_ip=request.META["REMOTE_ADDR"],
        comment_type="forum_post",
        comment_content=request.POST["post_body"],
        comment_author=request.user.username,
    )

    if classification == CheckResponse.DISCARD:
        # The post was "blatant" spam, reject it.
    elif classification == CheckResponse.SPAM:
        # Send it into the manual-review queue.
    elif classification == CheckResponse.HAM:
        # The post wasn't spam, allow it.

This works because the CheckResponse enum uses integer values; when fed directly to an if/else, they work as boolean values (HAM is 0, SPAM is 1, and DISCARD is 2).

Using a custom HTTP client

For some use cases, you may need custom HTTP client behavior. For example, you might be running on a server which has to use an HTTP proxy to access any external service. In that case, you can pass a custom HTTP client to the Akismet API client, as the constructor argument http_client. The Akismet API clients use the Python HTTPX library (which is automatically installed when you install akismet) for their HTTP clients, so you can create either an httpx.Client or an httpx.AsyncClient with the behavior you want.

You should also make sure to set a value for the User-Agent header of your custom HTTP client. If you want the default value the Akismet clients would use, it’s available as akismet.USER_AGENT.

import akismet
import httpx

from your_app.config import settings

akismet_client = akismet.SyncClient.validated_client(
    http_client=httpx.Client(
        proxy=settings.PROXY_URL,
        headers={"User-Agent": akismet.USER_AGENT}
    )
)
import akismet
import httpx

from your_app.config import settings

akismet_client = await akismet.AsyncClient.validated_client(
    http_client=httpx.AsyncClient(
        proxy=settings.PROXY_URL,
        headers={"User-Agent": akismet.USER_AGENT}
    )
)

Finally, note that if all you want is to set a custom timeout value for connections to the Akismet web service, you do not need a custom HTTP client; you can set the environment variable PYTHON_AKISMET_TIMEOUT as described above.

Testing your use of akismet

While you can perform limited end-to-end testing of Akismet’s spam-checking if you want to (see the testing guide for details), in general it’s discouraged to make live requests to external services as part of a normal application test suite.

It’s also generally discouraged to build extensive unittest.mock representations of code that isn’t under your control; this often leads to over-complicated test setups and a high maintenance burden as you attempt to keep your mocks in sync with what a third-party library is doing.

So akismet provides two test clients intended to be used in your application’s tests: TestAsyncClient as a test version of AsyncClient, and TestSyncClient as a test version of SyncClient.

Both of these test classes implement the full API of their real counterparts, but they do not make actual requests to the Akismet web service. You can configure them by subclassing and setting attributes to simulate content being marked as spam/not-spam and also to simulate an invalid API key. For example, you might write a simple spam-flagging function which toggles an attribute on a submitted comment:

def flag_spam_comment(akismet_client, request, comment):
    """
    If the submitted content is marked as spam by Akismet, set it to
    have filtered=True.

    """
    if akismet_client.comment_check(
        user_ip=request.META["REMOTE_ADDR"],
        comment_type="comment",
        comment_content=comment.body,
        comment_author=request.user.username,
    ):
        comment.filtered = True
    return comment

And then test it like so:

import unittest

import akismet

from your_app.moderation import flag_spam_comment
from your_app.test_factories import make_test_request, make_test_comment


class AlwaysSpam(akismet.TestSyncClient):
    """
    An Akismet client whose comment_check() always returns SPAM.

    """
    comment_check_response = akismet.CheckResponse.SPAM


class NeverSpam(akismet.TestSyncClient):
    """
    An Akismet client whose comment_check() always returns HAM.

    """
    comment_check_response = akismet.CheckResponse.HAM


class SpamFlagTests(unittest.TestCase):
   """
   Test the spam-flagging function.

   """
   def test_flag_set_on_spam(self):
       """
       When the comment is identified as spam, the "filtered" attribute
       is set to True.

       """
       with AlwaysSpam() as akismet_client:
           comment = flag_spam_comment(
               akismet_client,
               make_test_request(),
               make_test_comment()
           )
       assert comment.filtered

   def test_flag_not_set_on_non_spam(self):
       """
       When the comment is identified as non-spam, the "filtered" attribute
       is set to False.

       """
       with NeverSpam() as akismet_client:
           comment = flag_spam_comment(
               akismet_client,
               make_test_request(),
               make_test_comment()
           )
       assert not comment.filtered
import akismet
import pytest

from your_app.moderation import flag_spam_comment


# The following test functions assume you have also defined pytest
# fixtures to create the request and comment objects.
#
#
# A pytest plugin provided with akismet defines fixtures for
# sync and async clients, with behavior configured by the
# akismet_client mark.

@pytest.mark.akismet_client(comment_check_response=akismet.CheckResponse.SPAM)
def test_flag_set_on_spam(akismet_sync_client, test_request, test_comment):
    """
    When the comment is identified as spam, the "filtered" attribute
    is set to True.

    """
    comment = flag_spam_comment(
        akismet_sync_client,
        test_request,
        test_comment
    )
    assert comment.filtered


@pytest.mark.akismet_client(comment_check_response=akismet.CheckResponse.HAM)
def test_flag_not_set_on_non_spam(akismet_sync_client, test_request, test_comment):
    """
    When the comment is identified as non-spam, the "filtered" attribute
    is set to False.

    """
    comment = flag_spam_comment(
        akismet_sync_client,
        test_request,
        test_comment
    )
    assert not comment.filtered