growthbook 2.1.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,700 @@
1
+ Metadata-Version: 2.4
2
+ Name: growthbook
3
+ Version: 2.1.2
4
+ Summary: Powerful Feature flagging and A/B testing for Python apps
5
+ Home-page: https://github.com/growthbook/growthbook-python
6
+ Author: GrowthBook
7
+ Author-email: GrowthBook <hello@growthbook.io>
8
+ License: MIT
9
+ Keywords: growthbook
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.6
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: cryptography
26
+ Requires-Dist: typing_extensions
27
+ Requires-Dist: urllib3
28
+ Requires-Dist: dataclasses; python_version < "3.7"
29
+ Requires-Dist: async-generator; python_version < "3.7"
30
+ Requires-Dist: aiohttp>=3.6.0
31
+ Requires-Dist: importlib-metadata; python_version < "3.8"
32
+ Dynamic: author
33
+ Dynamic: home-page
34
+ Dynamic: license-file
35
+ Dynamic: requires-python
36
+
37
+ # GrowthBook Python SDK
38
+
39
+ ![Build Status](https://github.com/growthbook/growthbook-python/workflows/Build/badge.svg)
40
+ [![PyPI](https://img.shields.io/pypi/v/growthbook.svg?maxAge=2592000)](https://pypi.org/project/growthbook/)
41
+ [![PyPI](https://img.shields.io/pypi/pyversions/growthbook.svg)](https://pypi.org/project/growthbook/)
42
+
43
+ ## Overview
44
+
45
+ Powerful Feature flagging and A/B testing for Python apps.
46
+
47
+ - **Lightweight and fast**
48
+ - **Local evaluation**, no network requests required
49
+ - Flexible **targeting**
50
+ - **Use your existing event tracking** (GA, Segment, Mixpanel, custom)
51
+ - **Remote configuration** to change feature flags without deploying new code
52
+ - **Async support** with real-time feature updates
53
+ - Python 3.9+
54
+ - 100% test coverage
55
+
56
+ ## Installation
57
+
58
+ `pip install growthbook` (recommended) or copy `growthbook.py` into your project
59
+
60
+ ## Type Checking Support
61
+
62
+ The GrowthBook Python SDK is fully typed and includes inline type hints for all public APIs. This enables:
63
+
64
+ - **Better IDE support** with autocomplete and inline documentation
65
+ - **Type safety** - catch bugs at development time with mypy or other type checkers
66
+ - **Better code documentation** - types serve as inline documentation
67
+ - **Safer refactoring** - type checkers will catch breaking changes
68
+
69
+ To use type checking with mypy:
70
+
71
+ ```bash
72
+ pip install mypy
73
+ mypy your_code.py
74
+ ```
75
+
76
+ The SDK includes a `py.typed` marker file and is compliant with [PEP 561](https://www.python.org/dev/peps/pep-0561/).
77
+
78
+ ## Quick Usage
79
+
80
+ ```python
81
+ from growthbook import GrowthBook
82
+
83
+ # User attributes for targeting and experimentation
84
+ attributes = {
85
+ "id": "123",
86
+ "customUserAttribute": "foo"
87
+ }
88
+
89
+ def on_experiment_viewed(experiment, result):
90
+ # Use whatever event tracking system you want
91
+ analytics.track(attributes["id"], "Experiment Viewed", {
92
+ 'experimentId': experiment.key,
93
+ 'variationId': result.variationId
94
+ })
95
+
96
+ # Create a GrowthBook instance
97
+ gb = GrowthBook(
98
+ attributes = attributes,
99
+ on_experiment_viewed = on_experiment_viewed,
100
+ api_host = "https://cdn.growthbook.io",
101
+ client_key = "sdk-abc123"
102
+ )
103
+
104
+ # Load features from the GrowthBook API with caching
105
+ gb.load_features()
106
+
107
+ # Simple on/off feature gating
108
+ if gb.is_on("my-feature"):
109
+ print("My feature is on!")
110
+
111
+ # Get the value of a feature with a fallback
112
+ color = gb.get_feature_value("button-color-feature", "blue")
113
+ ```
114
+
115
+ ### Web Frameworks (Django, Flask, etc.)
116
+
117
+ For web frameworks, you should create a new `GrowthBook` instance for every incoming request and call `destroy()` at the end of the request to clean up resources.
118
+
119
+ In Django, for example, this is best done with a simple middleware:
120
+
121
+ ```python
122
+ from growthbook import GrowthBook
123
+
124
+ def growthbook_middleware(get_response):
125
+ def middleware(request):
126
+ request.gb = GrowthBook(
127
+ # ...
128
+ )
129
+ request.gb.load_features()
130
+
131
+ response = get_response(request)
132
+
133
+ request.gb.destroy() # Cleanup
134
+
135
+ return response
136
+ return middleware
137
+ ```
138
+
139
+ Then, you can easily use GrowthBook in any of your views:
140
+
141
+ ```python
142
+ def index(request):
143
+ feature_enabled = request.gb.is_on("my-feature")
144
+ # ...
145
+ ```
146
+
147
+ ## Quick Usage - Async Client
148
+
149
+ ```python
150
+ from growthbook import GrowthBookClient, Options, UserContext, FeatureRefreshStrategy
151
+ import asyncio
152
+
153
+ async def main():
154
+ # Create client options
155
+ options = Options(
156
+ api_host="https://cdn.growthbook.io",
157
+ client_key="sdk-abc123",
158
+ # Optional: Enable real-time feature updates
159
+ refresh_strategy=FeatureRefreshStrategy.SERVER_SENT_EVENTS
160
+ )
161
+
162
+ # Create and initialize client
163
+ client = GrowthBookClient(options)
164
+ try:
165
+ # Initialize the client before using it
166
+ success = await client.initialize()
167
+ if not success:
168
+ print("Failed to initialize GrowthBook client")
169
+ return
170
+
171
+ # Create user context for targeting
172
+ user = UserContext(
173
+ attributes={
174
+ "id": "123",
175
+ "country": "US",
176
+ "premium": True
177
+ }
178
+ )
179
+
180
+ # Simple feature evaluation
181
+ if await client.is_on("new-homepage", user):
182
+ print("New homepage is enabled!")
183
+
184
+ # Get feature value with fallback
185
+ color = await client.get_feature_value("button-color", "blue", user)
186
+ print(f"Button color is {color}")
187
+
188
+ # Run an experiment
189
+ result = await client.run(
190
+ Experiment(
191
+ key="my-test",
192
+ variations=["A", "B"]
193
+ ),
194
+ user
195
+ )
196
+ print(f"User got variation: {result.value}")
197
+ finally:
198
+ # Always close the client when done
199
+ await client.close()
200
+
201
+ # Run the async code
202
+ asyncio.run(main())
203
+ ```
204
+
205
+ ### Async Web Framework Integration
206
+
207
+ The async client works great with async web frameworks like FastAPI:
208
+
209
+ ```python
210
+ from fastapi import FastAPI, Depends
211
+ from growthbook import GrowthBookClient, Options, UserContext
212
+
213
+ app = FastAPI()
214
+
215
+ # Create a single client instance
216
+ gb_client = GrowthBookClient(
217
+ Options(
218
+ api_host="https://cdn.growthbook.io",
219
+ client_key="sdk-abc123"
220
+ )
221
+ )
222
+
223
+ @app.on_event("startup")
224
+ async def startup():
225
+ # Initialize the client when the app starts
226
+ await gb_client.initialize()
227
+
228
+ @app.on_event("shutdown")
229
+ async def shutdown():
230
+ # Clean up when the app shuts down
231
+ await gb_client.close()
232
+
233
+ @app.get("/")
234
+ async def root(user_id: str):
235
+ # Create user context for the request
236
+ user = UserContext(attributes={"id": user_id})
237
+
238
+ # Use features
239
+ show_new_ui = await gb_client.is_on("new-ui", user)
240
+ return {"new_ui": show_new_ui}
241
+ ```
242
+
243
+ ### Real-time Feature Updates
244
+
245
+ The async client supports real-time feature updates using Server-Sent Events:
246
+
247
+ ```python
248
+ from growthbook import GrowthBookClient, Options, FeatureRefreshStrategy
249
+
250
+ client = GrowthBookClient(
251
+ Options(
252
+ api_host="https://cdn.growthbook.io",
253
+ client_key="sdk-abc123",
254
+ # Enable SSE for real-time updates
255
+ refresh_strategy=FeatureRefreshStrategy.SERVER_SENT_EVENTS
256
+ )
257
+ )
258
+ ```
259
+
260
+ ### Concurrency and Thread Safety
261
+
262
+ The async client is designed to be thread-safe and handle concurrent requests efficiently. You can safely use a single client instance across multiple coroutines. For web applications, you can create a single client instance at startup and share it across requests. Here's an example:
263
+
264
+ ```python
265
+ from fastapi import FastAPI
266
+ from growthbook import GrowthBookClient, Options, UserContext
267
+ import asyncio
268
+
269
+ app = FastAPI()
270
+
271
+ # Single client instance shared across all requests
272
+ gb_client = GrowthBookClient(Options(
273
+ api_host="https://cdn.growthbook.io",
274
+ client_key="sdk-abc123"
275
+ ))
276
+
277
+ @app.on_event("startup")
278
+ async def startup():
279
+ await gb_client.initialize()
280
+
281
+ @app.on_event("shutdown")
282
+ async def shutdown():
283
+ await gb_client.close()
284
+
285
+ @app.get("/batch")
286
+ async def batch_process(user_ids: list[str]):
287
+ # Safely process multiple users concurrently
288
+ tasks = []
289
+ for user_id in user_ids:
290
+ user = UserContext(attributes={"id": user_id})
291
+ tasks.append(gb_client.eval_feature("new-feature", user))
292
+
293
+ results = await asyncio.gather(*tasks)
294
+ return {"results": results}
295
+ ```
296
+
297
+ Note: While the client is thread-safe, you should not share a single `UserContext` instance across different requests. Create a new `UserContext` for each request to maintain proper isolation.
298
+
299
+ ## Loading Features
300
+
301
+ There are two ways to load feature flags into the GrowthBook SDK. You can either use the built-in fetching/caching logic or implement your own custom solution.
302
+
303
+ ### Built-in Fetching and Caching
304
+
305
+ To use the built-in fetching and caching logic, in the `GrowthBook` constructor, pass in your GrowthBook `api_host` and `client_key`. If you have encryption enabled for your GrowthBook endpoint, you also need to pass the `decryption_key` into the constructor.
306
+
307
+ Then, call the `load_features()` method to initiate the HTTP request with a cache layer.
308
+
309
+ Here's a full example:
310
+
311
+ ```python
312
+ gb = GrowthBook(
313
+ api_host = "https://cdn.growthbook.io",
314
+ client_key = "sdk-abc123",
315
+ # How long to cache features in seconds (Optional, default 60s)
316
+ cache_ttl = 60,
317
+ )
318
+ gb.load_features()
319
+ ```
320
+
321
+ #### Caching
322
+
323
+ GrowthBook comes with a custom in-memory cache. If you run Python in a multi-process mode, the different processes cannot share memory, so you likely want to switch to a distributed cache system like Redis instead.
324
+
325
+ Here is an example of using Redis:
326
+
327
+ ```python
328
+ from redis import Redis
329
+ import json
330
+ from growthbook import GrowthBook, AbstractFeatureCache, feature_repo
331
+
332
+ class RedisFeatureCache(AbstractFeatureCache):
333
+ def __init__(self):
334
+ self.r = Redis(host='localhost', port=6379)
335
+ self.prefix = "gb:"
336
+
337
+ def get(self, key: str):
338
+ data = self.r.get(self.prefix + key)
339
+ # Data stored as a JSON string, parse into dict before returning
340
+ return None if data is None else json.loads(data)
341
+
342
+ def set(self, key: str, value: dict, ttl: int) -> None:
343
+ self.r.set(self.prefix + key, json.dumps(value))
344
+ self.r.expire(self.prefix + key, ttl)
345
+
346
+ # Configure GrowthBook to use your custom cache class
347
+ feature_repo.set_cache(RedisFeatureCache())
348
+ ```
349
+
350
+ ### Custom Implementation
351
+
352
+ If you prefer to handle the entire fetching/caching logic yourself, you can just pass in a `dict` of features from the GrowthBook API directly into the constructor:
353
+
354
+ ```python
355
+ # From the GrowthBook API
356
+ features = {'my-feature':{'defaultValue':False}}
357
+
358
+ gb = GrowthBook(
359
+ features = features
360
+ )
361
+ ```
362
+
363
+ Note: When doing this, you do not need to specify your `api_host` or `client_key` and you don't need to call `gb.load_features()`.
364
+
365
+ ## GrowthBook class
366
+
367
+ The GrowthBook constructor has the following parameters:
368
+
369
+ - **enabled** (`bool`) - Flag to globally disable all experiments. Default true.
370
+ - **attributes** (`dict`) - Dictionary of user attributes that are used for targeting and to assign variations
371
+ - **url** (`str`) - The URL of the current request (if applicable)
372
+ - **qa_mode** (`boolean`) - If true, random assignment is disabled and only explicitly forced variations are used.
373
+ - **on_experiment_viewed** (`callable`) - A function that takes `experiment` and `result` as arguments.
374
+ - **api_host** (`str`) - The GrowthBook API host to fetch feature flags from. Defaults to `https://cdn.growthbook.io`
375
+ - **client_key** (`str`) - The client key that will be passed to the API Host to fetch feature flags
376
+ - **decryption_key** (`str`) - If the GrowthBook API endpoint has encryption enabled, specify the decryption key here
377
+ - **cache_ttl** (`int`) - How long to cache features in-memory from the GrowthBook API (seconds, default `60`)
378
+ - **features** (`dict`) - Feature definitions from the GrowthBook API (only required if `client_key` is not specified)
379
+ - **forced_variations** (`dict`) - Dictionary of forced experiment variations (used for QA)
380
+
381
+ There are also getter and setter methods for features and attributes if you need to update them later in the request:
382
+
383
+ ```python
384
+ gb.set_features(gb.get_features())
385
+ gb.set_attributes(gb.get_attributes())
386
+ ```
387
+
388
+ ### Attributes
389
+
390
+ You can specify attributes about the current user and request. These are used for two things:
391
+
392
+ 1. Feature targeting (e.g. paid users get one value, free users get another)
393
+ 2. Assigning persistent variations in A/B tests (e.g. user id "123" always gets variation B)
394
+
395
+ Attributes can be any JSON data type - boolean, integer, float, string, list, or dict.
396
+
397
+ ```python
398
+ attributes = {
399
+ 'id': "123",
400
+ 'loggedIn': True,
401
+ 'age': 21.5,
402
+ 'tags': ["tag1", "tag2"],
403
+ 'account': {
404
+ 'age': 90
405
+ }
406
+ }
407
+
408
+ # Pass into constructor
409
+ gb = GrowthBook(attributes = attributes)
410
+
411
+ # Or set later
412
+ gb.set_attributes(attributes)
413
+ ```
414
+
415
+ ### Tracking Experiments
416
+
417
+ Any time an experiment is run to determine the value of a feature, you want to track that event in your analytics system.
418
+
419
+ You can use the `on_experiment_viewed` option to do this:
420
+
421
+ ```python
422
+ from growthbook import GrowthBook, Experiment, Result
423
+
424
+ def on_experiment_viewed(experiment: Experiment, result: Result):
425
+ # Use whatever event tracking system you want
426
+ analytics.track(attributes["id"], "Experiment Viewed", {
427
+ 'experimentId': experiment.key,
428
+ 'variationId': result.variationId
429
+ })
430
+
431
+ # Pass into constructor
432
+ gb = GrowthBook(
433
+ on_experiment_viewed = on_experiment_viewed
434
+ )
435
+ ```
436
+
437
+ #### Built-in Tracking Plugin
438
+
439
+ For easier setup, you can use the built-in tracking plugin that automatically sends experiment and feature events to GrowthBook's data warehouse:
440
+
441
+ ```python
442
+ from growthbook import GrowthBook
443
+ from growthbook.plugins import growthbook_tracking_plugin, request_context_plugin
444
+
445
+ gb = GrowthBook(
446
+ attributes={"id": "user-123"},
447
+ plugins=[
448
+ request_context_plugin(), # Extracts request data
449
+ growthbook_tracking_plugin(
450
+ ingestor_host="https://gb-ingest.growthbook.io",
451
+ # Optional: Add custom tracking callback
452
+ additional_callback=my_custom_tracker
453
+ )
454
+ ]
455
+ )
456
+
457
+ # Events are now automatically tracked for experiments and features
458
+ result = gb.run(experiment) # -> Tracked automatically
459
+ is_enabled = gb.is_on("my-feature") # -> Tracked automatically
460
+ ```
461
+
462
+ The tracking plugin provides batching, error handling, and works alongside your existing tracking callbacks. See the [plugin documentation](https://docs.growthbook.io/lib/python#tracking-plugins) for more details.
463
+
464
+ ## Using Features
465
+
466
+ There are 3 main methods for interacting with features.
467
+
468
+ - `gb.is_on("feature-key")` returns true if the feature is on
469
+ - `gb.is_off("feature-key")` returns false if the feature is on
470
+ - `gb.get_feature_value("feature-key", "default")` returns the value of the feature with a fallback
471
+
472
+ In addition, you can use `gb.evalFeature("feature-key")` to get back a `FeatureResult` object with the following properties:
473
+
474
+ - **value** - The JSON-decoded value of the feature (or `None` if not defined)
475
+ - **on** and **off** - The JSON-decoded value cast to booleans
476
+ - **source** - Why the value was assigned to the user. One of `unknownFeature`, `defaultValue`, `force`, or `experiment`
477
+ - **experiment** - Information about the experiment (if any) which was used to assign the value to the user
478
+ - **experimentResult** - The result of the experiment (if any) which was used to assign the value to the user
479
+
480
+ ## Sticky Bucketing
481
+
482
+ By default GrowthBook does not persist assigned experiment variations for a user. We rely on deterministic hashing to ensure that the same user attributes always map to the same experiment variation. However, there are cases where this isn't good enough. For example, if you change targeting conditions in the middle of an experiment, users may stop being shown a variation even if they were previously bucketed into it.
483
+
484
+ Sticky Bucketing is a solution to these issues. You can provide a Sticky Bucket Service to the GrowthBook instance to persist previously seen variations and ensure that the user experience remains consistent for your users.
485
+
486
+ A sample `InMemoryStickyBucketService` implementation is provided for reference, but in production you will definitely want to implement your own version using a database, cookies, or similar for persistence.
487
+
488
+ Sticky Bucket documents contain three fields
489
+
490
+ - `attributeName` - The name of the attribute used to identify the user (e.g. `id`, `cookie_id`, etc.)
491
+ - `attributeValue` - The value of the attribute (e.g. `123`)
492
+ - `assignments` - A dictionary of persisted experiment assignments. For example: `{"exp1__0":"control"}`
493
+
494
+ The attributeName/attributeValue combo is the primary key.
495
+
496
+ Here's an example implementation using a theoretical `db` object:
497
+
498
+ ```python
499
+ from growthbook import AbstractStickyBucketService, GrowthBook
500
+
501
+ class MyStickyBucketService(AbstractStickyBucketService):
502
+ # Lookup a sticky bucket document
503
+ def get_assignments(self, attributeName: str, attributeValue: str) -> Optional[Dict]:
504
+ return db.find({
505
+ "attributeName": attributeName,
506
+ "attributeValue": attributeValue
507
+ })
508
+
509
+ def save_assignments(self, doc: Dict) -> None:
510
+ # Insert new record if not exists, otherwise update
511
+ db.upsert({
512
+ "attributeName": doc["attributeName"],
513
+ "attributeValue": doc["attributeValue"]
514
+ }, {
515
+ "$set": {
516
+ "assignments": doc["assignments"]
517
+ }
518
+ })
519
+
520
+ # Pass in an instance of this service to your GrowthBook constructor
521
+ gb = GrowthBook(
522
+ sticky_bucket_service = MyStickyBucketService()
523
+ )
524
+ ```
525
+
526
+ ## Inline Experiments
527
+
528
+ Instead of declaring all features up-front and referencing them by ids in your code, you can also just run an experiment directly. This is done with the `run` method:
529
+
530
+ ```python
531
+ from growthbook import Experiment
532
+
533
+ exp = Experiment(
534
+ key = "my-experiment",
535
+ variations = ["red", "blue", "green"]
536
+ )
537
+
538
+ # Either "red", "blue", or "green"
539
+ print(gb.run(exp).value)
540
+ ```
541
+
542
+ As you can see, there are 2 required parameters for experiments, a string key, and an array of variations. Variations can be any data type, not just strings.
543
+
544
+ There are a number of additional settings to control the experiment behavior:
545
+
546
+ - **key** (`str`) - The globally unique tracking key for the experiment
547
+ - **variations** (`any[]`) - The different variations to choose between
548
+ - **seed** (`str`) - Added to the user id when hashing to determine a variation. Defaults to the experiment `key`
549
+ - **weights** (`float[]`) - How to weight traffic between variations. Must add to 1.
550
+ - **coverage** (`float`) - What percent of users should be included in the experiment (between 0 and 1, inclusive)
551
+ - **condition** (`dict`) - Targeting conditions
552
+ - **force** (`int`) - All users included in the experiment will be forced into the specified variation index
553
+ - **hashAttribute** (`string`) - What user attribute should be used to assign variations (defaults to "id")
554
+ - **hashVersion** (`int`) - What version of our hashing algorithm to use. We recommend using the latest version `2`.
555
+ - **namespace** (`tuple[str,float,float]`) - Used to run mutually exclusive experiments.
556
+
557
+ Here's an example that uses all of them:
558
+
559
+ ```python
560
+ exp = Experiment(
561
+ key="my-test",
562
+ # Variations can be a list of any data type
563
+ variations=[0, 1],
564
+ # If this changes, it will re-randomize all users in the experiment
565
+ seed="abcdef123456",
566
+ # Run a 40/60 experiment instead of the default even split (50/50)
567
+ weights=[0.4, 0.6],
568
+ # Only include 20% of users in the experiment
569
+ coverage=0.2,
570
+ # Targeting condition using a MongoDB-like syntax
571
+ condition={
572
+ 'country': 'US',
573
+ 'browser': {
574
+ '$in': ['chrome', 'firefox']
575
+ }
576
+ },
577
+ # Use an alternate attribute for assigning variations (default is 'id')
578
+ hashAttribute="sessionId",
579
+ # Use the latest hashing algorithm
580
+ hashVersion=2,
581
+ # Includes the first 50% of users in the "pricing" namespace
582
+ # Another experiment with a non-overlapping range will be mutually exclusive (e.g. [0.5, 1])
583
+ namespace=("pricing", 0, 0.5),
584
+ )
585
+ ```
586
+
587
+ ### Inline Experiment Return Value
588
+
589
+ A call to `run` returns a `Result` object with a few useful properties:
590
+
591
+ ```python
592
+ result = gb.run(exp)
593
+
594
+ # If user is part of the experiment
595
+ print(result.inExperiment) # True or False
596
+
597
+ # The index of the assigned variation
598
+ print(result.variationId) # e.g. 0 or 1
599
+
600
+ # The value of the assigned variation
601
+ print(result.value) # e.g. "A" or "B"
602
+
603
+ # If the variation was randomly assigned by hashing user attributes
604
+ print(result.hashUsed) # True or False
605
+
606
+ # The user attribute used to assign a variation
607
+ print(result.hashAttribute) # "id"
608
+
609
+ # The value of that attribute
610
+ print(result.hashValue) # e.g. "123"
611
+ ```
612
+
613
+ The `inExperiment` flag will be false if the user was excluded from being part of the experiment for any reason (e.g. failed targeting conditions).
614
+
615
+ The `hashUsed` flag will only be true if the user was randomly assigned a variation. If the user was forced into a specific variation instead, this flag will be false.
616
+
617
+ ### Example Experiments
618
+
619
+ 3-way experiment with uneven variation weights:
620
+
621
+ ```python
622
+ gb.run(Experiment(
623
+ key = "3-way-uneven",
624
+ variations = ["A","B","C"],
625
+ weights = [0.5, 0.25, 0.25]
626
+ ))
627
+ ```
628
+
629
+ Slow rollout (10% of users who match the targeting condition):
630
+
631
+ ```python
632
+ # User is marked as being in "qa" and "beta"
633
+ gb = GrowthBook(
634
+ attributes = {
635
+ "id": "123",
636
+ "beta": True,
637
+ "qa": True,
638
+ },
639
+ )
640
+
641
+ gb.run(Experiment(
642
+ key = "slow-rollout",
643
+ variations = ["A", "B"],
644
+ coverage = 0.1,
645
+ condition = {
646
+ 'beta': True
647
+ }
648
+ ))
649
+ ```
650
+
651
+ Complex variations
652
+
653
+ ```python
654
+ result = gb.run(Experiment(
655
+ key = "complex-variations",
656
+ variations = [
657
+ ("blue", "large"),
658
+ ("green", "small")
659
+ ],
660
+ ))
661
+
662
+ # Either "blue,large" OR "green,small"
663
+ print(result.value[0] + "," + result.value[1])
664
+ ```
665
+
666
+ Assign variations based on something other than user id
667
+
668
+ ```python
669
+ gb = GrowthBook(
670
+ attributes = {
671
+ "id": "123",
672
+ "company": "growthbook"
673
+ }
674
+ )
675
+
676
+ # Users in the same company will always get the same variation
677
+ gb.run(Experiment(
678
+ key = "by-company-id",
679
+ variations = ["A", "B"],
680
+ hashAttribute = "company"
681
+ ))
682
+ ```
683
+
684
+ ## Logging
685
+
686
+ The GrowthBook SDK uses a Python logger with the name `growthbook` and includes helpful info for debugging as well as warnings/errors if something is misconfigured.
687
+
688
+ Here's an example of logging to the console
689
+
690
+ ```python
691
+ import logging
692
+
693
+ logger = logging.getLogger('growthbook')
694
+ logger.setLevel(logging.DEBUG)
695
+
696
+ handler = logging.StreamHandler()
697
+ formatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s %(message)s')
698
+ handler.setFormatter(formatter)
699
+ logger.addHandler(handler)
700
+ ```